This is a first pass exploration of different aspects of beer.
- The main question on the table is this:
- Are beer styles actually indicative of shared attributes of the beers within that style? Or are style boundaries more or less arbitrary?
- Two approaches: clustering and prediction
- Clustering: are there natural clusters across the spectum of beers that align well with the styles they’re grouped into?
- Unsupervised (k-means) clustering based on
- ABV (alcohol by volume), IBU (international bitterness units), SRM (measure of color)
- How well do these match up with various “style centers,” defined by mean of ABV, IBU, and SRM per beer style
- Prediction: can we predict a beer’s style based on certain characteristics of the beer?
- Answer thus far
- Beer-intrinsic attributes aren’t great predictors of style
- Style-defined attributes are the best predictors
- For instance, the glass a beer is served in (which is defined by its style) is a much better predictor of its style than actual characteristics of the beer like ABV and even the number of different types of hops it contains
Workflow Overview
- Hit the BreweryDB API to iteratively pull in all beers and their ingredients along with other things we might want like breweries and glassware
- Unnest the JSON responses, including all the ingredients columns, and
Dump this all into a MySQL database
- Create a
style_collapsed column to reduce the number of levels of our outcome variable
grep through each beer’s style to determine if that style contains a keyword that qualifies it to be rolled into a collapsed style
- If it does, it gets that keyword in a
style_collapsed column
- Further collpase styles that are similar like Hefeweizen and Wit into Wheat
- Unnest the ingredients
hops and malts into a sparse matrix
- Individual ingredients as columns, beers as rows; cell gets a 1 if ingredient is present and 0 otherwise
Cluster: unsupervised k-means clsutering based on ABV, IBU, and SRM
- Run a neural net
- Predict either
style or style_collapsed from all the predictors including the total number of hops and malts per beer
Short Aside
The question of what should be a predictor variable for style is a bit murky here. What should be fair game for predicting style and what shouldn’t? Characteristics of a beer that are defined by its style would seem to be “cheating” in a way.
- Main candidates are:
- ABV (alcohol by volume), IBU (international bitterness units), SRM (standard reference measure, a scale of beer color from light to dark)
- These are outputs of a beer that meaningfully define the beer and are theoretically orthogonal to each other
- Ingredients in a beer such as hops and malts
- Inputs to a beer that have some effect on its flavor profile
- Semi-cheating because if style is determined beforehand it likely determines at least in part which ingredients are added
- Glass type
- This is defined entirely by style and is very predictive of it
This document compiled by querying the beer database I built, specifically by sourcing the file read_from_db.R. This is done for expediency’s sake, (the code below detailing how to get the beer, run in full in run_it.R, takes some time to execute).
Get and Prepare Data
Getting beer, the age-old dilemma
- The BreweryDB API returns a certain number of results per page; if we want
- So, we hit the BreweryDB API and ask for
1:number_of_pages
- We can change
number_of_pages to, e.g., 3 if we only want the first 3 pages
- If there’s only one page (as is the case for the glassware endpoing), numberOfPages won’t be returned, so in this case we set number_of_pages to 1
- The
addition parameter can be an empty string if nothing else is needed
base_url <- "http://api.brewerydb.com/v2"
key_preface <- "/?key="
paginated_request <- function(ep, addition) {
full_request <- NULL
first_page <- fromJSON(paste0(base_url, "/", ep, "/", key_preface, key
, "&p=1"))
number_of_pages <- ifelse(!(is.null(first_page$numberOfPages)),
first_page$numberOfPages, 1)
for (page in 1:number_of_pages) {
this_request <- fromJSON(paste0(base_url, "/", ep, "/", key_preface, key
, "&p=", page, addition),
flatten = TRUE)
this_req_unnested <- unnest_it(this_request) # <- request unnested here
print(this_req_unnested$currentPage)
full_request <- bind_rows(full_request, this_req_unnested[["data"]])
}
full_request
}
all_beer_raw <- paginated_request("beers", "&withIngredients=Y")
- Function for unnesting JSON used inside
paginated_request() below
- Takes the column named
name nested within a column in the data portion of the response
- If the
name column doesn’t exist, it takes the first nested column
- We use something similar to unnest ingredient like all of a beer’s hops and malts into a long string contained in
hops_name and malt_name
unnest_it <- function(df) {
unnested <- df
for(col in seq_along(df[["data"]])) {
if(! is.null(ncol(df[["data"]][[col]]))) {
if(! is.null(df[["data"]][[col]][["name"]])) {
unnested[["data"]][[col]] <- df[["data"]][[col]][["name"]]
} else {
unnested[["data"]][[col]] <- df[["data"]][[col]][[1]]
}
}
}
unnested
}
Collapse Styles
- Save the most popular styles in
keywords
- Loop through each keyword
- For each beer,
grep through its style column to see if it contains any one of these keywords
- If it does, give it that keyword in a new column
style_collapsed
- If a beer’s name matches multiple keywords, e.g., American Double India Pale Ale would match Double India Pale Ale, India Pale Ale, and Pale Ale, its
style_collapsed is the last of those that appear in keywords
- This is why keywords are intentionally ordered from most general to most specific
- So in the case of an case of American Double India Pale Ale: since Double India Pale Ale appears in
keywords after India Pale Ale and Pale Ale, an American Double India Pale Ale would get a style_collapsed of Double India Pale Ale
- If no keyword is contained in
style, style_collapsed is just whatever’s in style; in other words, it doesn’t get collpsed into a bigger bucket
- This isn’t a huge problem because we’ll pare down to just the most popular styles later, however we could think about creating a catchall “Other” level for
style_collapsed
collapse_styles <- function(df) {
keywords <- c("Lager", "Pale Ale", "India Pale Ale", "Double India Pale Ale", "India Pale Lager", "Hefeweizen", "Barrel-Aged","Wheat", "Pilsner", "Pilsener", "Amber", "Golden", "Blonde", "Brown", "Black", "Stout", "Porter", "Red", "Sour", "Kölsch", "Tripel", "Bitter", "Saison", "Strong Ale", "Barley Wine", "Dubbel", "Altbier")
for (beer in 1:nrow(df)) {
if (grepl(paste(keywords, collapse="|"), df$style[beer])) {
for (keyword in keywords) {
if(grepl(keyword, df$style[beer]) == TRUE) {
df$style_collapsed[beer] <- keyword
}
}
} else {
df$style_collapsed[beer] <- as.character(df$style[beer])
}
print(df$style_collapsed[beer])
}
return(df)
}
- Then we collapse further; right now we just combine all wheaty bears into Wheat and Pils-like beers into Pilsener (with two e’s) by
fct_collapseing those levels
collapse_further <- function(df) {
df[["style_collapsed"]] <- df[["style_collapsed"]] %>%
fct_collapse(
"Wheat" = c("Hefeweizen", "Wheat"),
"Pilsener" = c("Pilsner", "American-Style Pilsener") # pilsener == pilsner == pils
)
return(df)
}
Split out Ingredients
- When we unnested ingredients, we just concatenated all of the ingredients for a given beer into a long string
- If we want, we can split out the ingredients that were concatenated in
<ingredient>_name with this split_ingredients function
This takes a vector of ingredients_to_split, so e.g. c("hops_name", "malt_name") and creates one column for each type of ingredient (hops_name_1, hops_name_2, etc.)
- We
str_split on the ingredient and get a list back
- We find the max number of instances of an ingredient per beer, which will be the number of columns we’re adding
- For each new column we need, we create it, initialize it with NAs, and name it
Then for each element in our list of split up ingredients, if it exists, we add it to the correct column in our df
split_ingredients <- function(df, ingredients_to_split) {
ncol_df <- ncol(df)
for (ingredient in ingredients_to_split) {
ingredient_split <- str_split(df[[ingredient]], ", ")
num_new_cols <- max(lengths(ingredient_split))
for (num in 1:num_new_cols) {
this_col <- ncol_df + 1
df[, this_col] <- NA
names(df)[this_col] <- paste0(ingredient, "_", num)
ncol_df <- ncol(df)
for (row in seq_along(ingredient_split)) {
if (!is.null(ingredient_split[[row]][num])) {
df[row, this_col] <- ingredient_split[[row]][num]
}
}
df[[names(df)[this_col]]] <- factor(df[[names(df)[this_col]]])
}
ncol_df <- ncol(df)
}
return(df)
}
Find the Most Popualar Styles
- Find mean ABV, IBU, and SRM per collapsed style
- Arrange collapsed styles by the number of beers that fall into them
- This is of course dependent on how we collapse styles
library(forcats)
# Pare down to only cases where style is not NA
beer_dat <- beer_necessities
beer_dat_pared <- beer_dat[complete.cases(beer_dat$style), ]
# Arrange beer dat by style popularity
style_popularity <- beer_dat_pared %>%
group_by(style) %>%
count() %>%
arrange(desc(n))
style_popularity
# Add a column that scales popularity
style_popularity <- bind_cols(style_popularity,
n_scaled = as.vector(scale(style_popularity$n)))
# Find styles that are above a z-score of 0
popular_styles <- style_popularity %>%
filter(n_scaled > 0)
# Pare dat down to only beers that fall into those styles
popular_beer_dat <- beer_dat_pared %>%
filter(
style %in% popular_styles$style
) %>%
droplevels() %>%
as_tibble()
nrow(popular_beer_dat)
# Find the centers (mean abv, ibu, srm) of the most popular styles
style_centers <- popular_beer_dat %>%
group_by(style_collapsed) %>%
add_count() %>%
summarise(
mean_abv = mean(abv, na.rm = TRUE),
mean_ibu = mean(ibu, na.rm = TRUE),
mean_srm = mean(srm, na.rm = TRUE),
n = median(n, na.rm = TRUE) # Median here only for summarise. Should be just the same as n
) %>%
arrange(desc(n)) %>%
drop_na() %>%
droplevels()
# Give some nicer names
style_centers_rename <- style_centers %>%
rename(
`Collapsed Style` = style_collapsed,
`Mean ABV` = mean_abv,
`Mean IBU` = mean_ibu,
`Mean SRM` = mean_srm,
`Numer of Beers` = n
)
Take a look at the table
kable(style_centers_rename)
| India Pale Ale |
6.578468 |
66.04268 |
9.989313 |
6524 |
| Pale Ale |
5.695480 |
40.86930 |
8.890306 |
4280 |
| Stout |
7.991841 |
43.89729 |
36.300000 |
4238 |
| Wheat |
5.158040 |
17.47168 |
5.861842 |
3349 |
| Double India Pale Ale |
8.930599 |
93.48142 |
11.006873 |
2525 |
| Red |
5.742565 |
33.81127 |
16.178862 |
2521 |
| Lager |
5.453718 |
30.64361 |
8.457447 |
2230 |
| Saison |
6.400189 |
27.25114 |
7.053476 |
2167 |
| Blonde |
5.595298 |
22.39432 |
5.625000 |
2044 |
| Porter |
6.182049 |
33.25369 |
32.197605 |
1973 |
| Brown |
6.159212 |
32.21577 |
23.592000 |
1462 |
| Pilsener |
5.227593 |
33.51346 |
4.413462 |
1268 |
| Specialty Beer |
6.446402 |
33.77676 |
15.520548 |
1044 |
| Bitter |
5.322364 |
38.28175 |
12.460526 |
939 |
| Fruit Beer |
5.195222 |
19.24049 |
8.666667 |
905 |
| Herb and Spice Beer |
6.621446 |
27.77342 |
18.166667 |
872 |
| Sour |
6.224316 |
18.88869 |
10.040816 |
797 |
| Strong Ale |
8.826425 |
36.74233 |
22.547945 |
767 |
| Tripel |
9.029775 |
32.51500 |
7.680556 |
734 |
| Black |
6.958714 |
65.50831 |
31.080000 |
622 |
| Barley Wine |
10.781600 |
74.04843 |
19.561404 |
605 |
| Kölsch |
4.982216 |
23.37183 |
4.371795 |
593 |
| Barrel-Aged |
9.002506 |
39.15789 |
18.133333 |
540 |
| Other Belgian-Style Ales |
7.516318 |
37.55812 |
17.549020 |
506 |
| Pumpkin Beer |
6.712839 |
23.48359 |
17.918033 |
458 |
| Dubbel |
7.509088 |
25.05128 |
22.940000 |
399 |
| Scotch Ale |
7.620233 |
26.36909 |
24.222222 |
393 |
| German-Style Doppelbock |
8.045762 |
28.88692 |
25.696970 |
376 |
| Fruit Cider |
6.205786 |
25.60000 |
12.000000 |
370 |
| German-Style Märzen |
5.746102 |
25.63796 |
14.322581 |
370 |
Now that the munging is done, onto the main question: do natural clusters in beer align with style boundaries?
Ingredients
To get more granular with ingredients, we can split out each individual ingredient into its own column. If a beer or style contains that ingredient, its row gets a 1 in that ingredient column and a 0 otherwise.
From this, we can find the total number of hops and malts per grouper.
- The dataframe we’ll use will be
beer_necessities
- This function takes a dataframe and two other parameters set at the outset:
ingredient_want: this can be hops, malt, or other ingredients like yeast if we pull that in
grouper: can be a vector of one or more things to group by, like beer name or style
pick_ingredient_get_beer <- function (ingredient_want, df, grouper) {
# ----------------------- Setup --------------------------- #
# We've already split ingredient number names out from the concatenated string into columns like `malt_name_1`,
# `malt_name_2`, etc. We need to find the range of these columns; there will be a different number of malt
# columns than hops columns, for instance. The first one will be `<ingredient>_name_1` and from this we can find
# the index of this column in our dataframe. We get the name of last one with the `get_last_ing_name_col()`
# function. Then we save a vector of all the ingredient column names in `ingredient_colnames`. It will stay
# constant even if the indices change when we select out certain columns.
# First ingredient
first_ingredient_name <- paste(ingredient_want, "_name_1", sep="")
first_ingredient_index <- which(colnames(df)==first_ingredient_name)
# Get the last ingredient
get_last_ing_name_col <- function(df) {
for (col in names(df)) {
if (grepl(paste(ingredient_want, "_name_", sep = ""), col) == TRUE) {
name_last_ing_col <- col
}
}
return(name_last_ing_col)
}
# Last ingredient
last_ingredient_name <- get_last_ing_name_col(df)
last_ingredient_index <- which(colnames(df)==last_ingredient_name)
# Vector of all the ingredient column names
ingredient_colnames <- names(df)[first_ingredient_index:last_ingredient_index]
# Non-ingredient column names we want to keep
to_keep_col_names <- c("cluster_assignment", "name", "abv", "ibu", "srm", "style", "style_collapsed")
# -------------------------------------------------------------------------------#
# Inside `gather_ingredients()` we take out superflous column names that are not in `to_keep_col_names` or one
# of the ingredient columns, find what the new ingredient column indices are, since they'll have changed after
# we pared down and then gather all of the ingredient columns (e.g., `hops_name_1`) into one long column,
# `ing_keys` and all the actual ingredient names (e.g., Cascade) into `ing_names`.
# ----------------------------- Gather columns --------------------------------- #
gather_ingredients <- function(df, cols_to_gather) {
to_keep_indices <- which(colnames(df) %in% to_keep_col_names)
selected_df <- df[, c(to_keep_indices, first_ingredient_index:last_ingredient_index)]
new_ing_indices <- which(colnames(selected_df) %in% cols_to_gather) # indices will have changed since we pared down
df_gathered <- selected_df %>%
gather_(
key_col = "ing_keys",
value_col = "ing_names",
gather_cols = colnames(selected_df)[new_ing_indices]
) %>%
mutate(
count = 1
)
df_gathered
}
beer_gathered <- gather_ingredients(df, ingredient_colnames) # ingredient colnames defined above function
# ------------------------------------------------------------------------------- #
# Next we get a vector of all ingredient levels and take out the one that's an empty string and
# use this vector of ingredient levels in `select_spread_cols()` below
# Get a vector of all ingredient levels
beer_gathered$ing_names <- factor(beer_gathered$ing_names)
ingredient_levels <- levels(beer_gathered$ing_names)
# Take out the level that's just an empty string
to_keep_levels <- !(c(1:length(ingredient_levels)) %in% which(ingredient_levels == ""))
ingredient_levels <- ingredient_levels[to_keep_levels]
beer_gathered$ing_names <- as.character(beer_gathered$ing_names)
# ----------------------------------------------------------------------------- #
# Then we spread the ingredient names: we take what was previously the `value` in our gathered dataframe, the
# actual ingredient names (Cascade, Centennial) and make that our `key`; it'll form the new column names. The
# new `value` is `value` is count; it'll populate the row cells. If a given row has a certain ingredient, it
# gets a 1 in the corresponding cell, an NA otherwise.
# We add a unique idenfitier for each row with `row`, which we'll drop later (see [Hadley's SO
# comment](https://stackoverflow.com/questions/25960394/unexpected-behavior-with-tidyr)).
# ------------------------------- Spread columns -------------------------------- #
spread_ingredients <- function(df) {
df_spread <- df %>%
mutate(
row = 1:nrow(df) # Add a unique idenfitier for each row which we'll need in order to spread; we'll drop this later
) %>%
spread(
key = ing_names,
value = count
)
return(df_spread)
}
beer_spread <- spread_ingredients(beer_gathered)
# ------------------------------------------------------------------------------- #
# ------------------------- Select only certain columns ------------------------- #
select_spread_cols <- function(df) {
to_keep_col_indices <- which(colnames(df) %in% to_keep_col_names)
to_keep_ingredient_indices <- which(colnames(df) %in% ingredient_levels)
to_keep_inds_all <- c(to_keep_col_indices, to_keep_ingredient_indices)
new_df <- df %>%
select_(
.dots = to_keep_inds_all
)
return(new_df)
}
beer_spread_selected <- select_spread_cols(beer_spread)
# ------------------------------------------------------------------------------- #
# Take out all rows that have no ingredients specified at all
inds_to_remove <- apply(beer_spread_selected[, first_ingredient_index:last_ingredient_index],
1, function(x) all(is.na(x)))
beer_spread_no_na <- beer_spread_selected[ !inds_to_remove, ]
# ----------------- Group ingredients by the grouper specified ------------------- #
# Then we do the final step and group by the groupers.
get_ingredients_per_grouper <- function(df, grouper = grouper) {
df_grouped <- df %>%
ungroup() %>%
group_by_(grouper)
not_for_summing <- which(colnames(df_grouped) %in% to_keep_col_names)
max_not_for_summing <- max(not_for_summing)
per_grouper <- df_grouped %>%
select(-c(abv, ibu, srm)) %>% # taking out temporarily
summarise_if(
is.numeric,
sum, na.rm = TRUE
# -c(abv, ibu, srm)
) %>%
mutate(
total = rowSums(.[(max_not_for_summing + 1):ncol(.)], na.rm = TRUE)
)
# Send total to the second position
per_grouper <- per_grouper %>%
select(
name, total, everything()
)
# Replace total column with more descriptive name: total_<ingredient>
names(per_grouper)[which(names(per_grouper) == "total")] <- paste0("total_", ingredient_want)
return(per_grouper)
}
# ------------------------------------------------------------------------------- #
ingredients_per_grouper <- get_ingredients_per_grouper(beer_spread_selected, grouper)
return(ingredients_per_grouper)
}
- Now run the function with
ingredient_want as first hops, then malt
- Then join the resulting dataframes and remove/reorder some columns
# Run the entire function with ingredient_want set to hops, grouping by name
ingredients_per_beer_hops <- pick_ingredient_get_beer(ingredient_want = "hops",
beer_necessities,
grouper = c("name", "style_collapsed"))
# Same for malt
ingredients_per_beer_malt <- pick_ingredient_get_beer(ingredient_want = "malt",
beer_necessities,
grouper = c("name", "style_collapsed"))
# Join those on our original dataframe by name
beer_ingredients_join_first_ingredient <- left_join(beer_necessities, ingredients_per_beer_hops,
by = "name")
beer_ingredients_join <- left_join(beer_ingredients_join_first_ingredient, ingredients_per_beer_malt,
by = "name")
# Take out some unnecessary columns
unnecessary_cols <- c("styleId", "abv_scaled", "ibu_scaled", "srm_scaled",
"hops_id", "malt_id", "glasswareId", "style.categoryId")
beer_ingredients_join <- beer_ingredients_join[, (! names(beer_ingredients_join) %in% unnecessary_cols)]
# If we also want to take out any of the malt_name_1, malt_name_2, etc. columns we can do this with a grep
more_unnecessary <- c("hops_name_|malt_name_")
beer_ingredients_join <-
beer_ingredients_join[, (! grepl(more_unnecessary, names(beer_ingredients_join)) == TRUE)]
# Reorder columns a bit
beer_ingredients_join <- beer_ingredients_join %>%
select(
id, name, total_hops, total_malt, everything(), -description
)
# And get a df that includes total_hops and total_malt but not all the other ingredient columns
beer_totals <- beer_ingredients_join %>%
select(
id, name, total_hops, total_malt, style, style_collapsed,
abv, ibu, srm, glass, hops_name, malt_name
)
Now we’re left with something of a sparse matrix of all the ingredients compared to all the beers
kable(beer_ingredients_join[1:20, ])
Unsupervised Clustering
We K-means cluster beers based on certain numeric predictor variables.
Prep
- Write a funciton that takes a dataframe, a set of predictors, a response variable, and the number of cluster centers you want
- NB: There are not not very many beers have SRM so we may not want to omit based on it
- Take out missing values, and scale the data
- Take out outliers, defined as beers have to have an ABV between 3 and 20 and an IBU less than 200
- Then cluster on just the predictors and compare to the response variable
library(NbClust)
cluster_it <- function(df, preds, to_scale, resp, n_centers) {
df_for_clustering <- df %>%
select_(.dots = c(response_vars, cluster_on)) %>%
na.omit() %>%
filter(
abv < 20 & abv > 3
) %>%
filter(
ibu < 200
)
df_all_preds <- df_for_clustering %>%
select_(.dots = preds)
df_preds_scale <- df_all_preds %>%
select_(.dots = to_scale) %>%
rename(
abv_scaled = abv,
ibu_scaled = ibu,
srm_scaled = srm
) %>%
scale() %>%
as_tibble()
df_preds <- bind_cols(df_preds_scale, df_all_preds[, (!names(df_all_preds) %in% to_scale)])
df_outcome <- df_for_clustering %>%
select_(.dots = resp) %>%
na.omit()
set.seed(9)
clustered_df_out <- kmeans(x = df_preds, centers = n_centers, trace = TRUE)
clustered_df <- as_tibble(data.frame(
cluster_assignment = factor(clustered_df_out$cluster),
df_outcome, df_preds,
df_for_clustering %>% select(abv, ibu, srm)))
return(clustered_df)
}
Cluster
First we’ll run the fuction with 10 centers, and cluster on the predictors ABV, IBU, SRM, total_hops, and total_malt.
cluster_on <- c("abv", "ibu", "srm", "total_hops", "total_malt")
to_scale <- c("abv", "ibu", "srm", "total_hops", "total_malt")
response_vars <- c("name", "style", "style_collapsed")
clustered_beer <- cluster_it(df = beer_totals,
preds = cluster_on,
to_scale = to_scale,
resp = response_vars,
n_centers = 10)
KMNS(*, k=10): iter= 1, indx=5
QTRAN(): istep=4472, icoun=1
QTRAN(): istep=8944, icoun=84
QTRAN(): istep=13416, icoun=31
QTRAN(): istep=17888, icoun=310
QTRAN(): istep=22360, icoun=444
QTRAN(): istep=26832, icoun=3571
KMNS(*, k=10): iter= 2, indx=28
QTRAN(): istep=4472, icoun=3
QTRAN(): istep=8944, icoun=26
QTRAN(): istep=13416, icoun=0
QTRAN(): istep=17888, icoun=45
QTRAN(): istep=22360, icoun=37
QTRAN(): istep=26832, icoun=170
QTRAN(): istep=31304, icoun=45
QTRAN(): istep=35776, icoun=64
QTRAN(): istep=40248, icoun=692
KMNS(*, k=10): iter= 3, indx=6
QTRAN(): istep=4472, icoun=16
QTRAN(): istep=8944, icoun=36
QTRAN(): istep=13416, icoun=318
QTRAN(): istep=17888, icoun=1114
QTRAN(): istep=22360, icoun=1628
QTRAN(): istep=26832, icoun=1518
KMNS(*, k=10): iter= 4, indx=227
QTRAN(): istep=4472, icoun=560
QTRAN(): istep=8944, icoun=1426
QTRAN(): istep=13416, icoun=401
KMNS(*, k=10): iter= 5, indx=4472
Head of the clustering data
kable(clustered_beer[1:20, ])
A table of cluster counts broken down by style
cluster_table_counts <- table(style = clustered_beer$style_collapsed, cluster = clustered_beer$cluster_assignment)
kable(cluster_table_counts)
Plot the clusters. There are 3 axes: ABV, IBU, and SRM, so we choose two at a time.
clustered_beer_plot_abv_ibu <- ggplot(data = clustered_beer, aes(x = abv, y = ibu, colour = cluster_assignment)) +
geom_jitter() + theme_minimal() +
ggtitle("k-Means Clustering of Beer by ABV, IBU, SRM") +
labs(x = "ABV", y = "IBU") +
labs(colour = "Cluster Assignment")
clustered_beer_plot_abv_ibu

clustered_beer_plot_abv_srm <- ggplot(data = clustered_beer, aes(x = abv, y = srm, colour = cluster_assignment)) +
geom_jitter() + theme_minimal() +
ggtitle("k-Means Clustering of Beer by ABV, IBU, SRM") +
labs(x = "ABV", y = "SRM") +
labs(colour = "Cluster Assignment")
clustered_beer_plot_abv_srm

Now we can add in the style centers (means) for each style_collapsed and label it.
library(ggrepel)
abv_ibu_clusters_vs_style_centers <- ggplot() +
geom_point(data = clustered_beer,
aes(x = abv, y = ibu, colour = cluster_assignment), alpha = 0.5) +
geom_point(data = style_centers,
aes(mean_abv, mean_ibu), colour = "black") +
geom_text_repel(data = style_centers, aes(mean_abv, mean_ibu, label = style_collapsed),
box.padding = unit(0.45, "lines"),
family = "Calibri",
label.size = 0.3) +
ggtitle("Popular Styles vs. k-Means Clustering of Beer by ABV, IBU, SRM") +
labs(x = "ABV", y = "IBU") +
labs(colour = "Cluster Assignment") +
theme_bw()
abv_ibu_clusters_vs_style_centers

The clustering above used a smaller number of clusters (10) than there are styles_collapsed. That makes it difficult to determine whether a given style fits snugly into a cluster or not.
Cluster on just certain selected styles
We’ll take five very distinct collapsed styles and re-run the clustering on beers that fall into these categories. These styles were intentionally chosen because they are quite distinct: Blonde, IPA, Stout, Tripel, Wheat. Arguably, of these five styles Blondes and Wheats are the closest
styles_to_keep <- c("Blonde", "India Pale Ale", "Stout", "Tripel", "Wheat")
bt_certain_styles <- beer_totals %>%
filter(
style_collapsed %in% styles_to_keep
)
cluster_on <- c("abv", "ibu", "srm", "total_hops", "total_malt")
to_scale <- c("abv", "ibu", "srm", "total_hops", "total_malt")
response_vars <- c("name", "style", "style_collapsed")
certain_styles_clustered <- cluster_it(df = bt_certain_styles,
preds = cluster_on,
to_scale = to_scale,
resp = response_vars,
n_centers = 5)
KMNS(*, k=5): iter= 1, indx=4
QTRAN(): istep=1322, icoun=2
QTRAN(): istep=2644, icoun=20
QTRAN(): istep=3966, icoun=96
QTRAN(): istep=5288, icoun=0
QTRAN(): istep=6610, icoun=13
QTRAN(): istep=7932, icoun=285
KMNS(*, k=5): iter= 2, indx=13
QTRAN(): istep=1322, icoun=147
QTRAN(): istep=2644, icoun=138
QTRAN(): istep=3966, icoun=20
QTRAN(): istep=5288, icoun=599
KMNS(*, k=5): iter= 3, indx=20
QTRAN(): istep=1322, icoun=146
QTRAN(): istep=2644, icoun=617
KMNS(*, k=5): iter= 4, indx=1322
style_centers_certain_styles <- style_centers %>%
filter(style_collapsed %in% styles_to_keep)
Table of style vs. cluster.
kable(table(style = certain_styles_clustered$style_collapsed, cluster = certain_styles_clustered$cluster_assignment))
Now that we have a manageable number of styles, we can see how well fit each cluster is to each style. If the features we clustered on perfectly predicted style, there would each color (cluster) would be unique to each facet of the plot. (E.g., left entirely blue, second from left entirely green, etc.)
by_style_plot <- ggplot() +
geom_point(data = certain_styles_clustered,
aes(x = abv, y = ibu,
colour = cluster_assignment), alpha = 0.5) +
facet_grid(. ~ style_collapsed) +
geom_point(data = style_centers_certain_styles,
aes(mean_abv, mean_ibu), colour = "black", shape = 5) +
ggtitle("Selected Styles Cluster Assignment") +
labs(x = "ABV", y = "IBU") +
labs(colour = "Cluster") +
theme_bw()
by_style_plot
–>
Random asides into hops
Do more hops always mean more bitterness?
- It would appear so, from this graph and this regression (beta = 2.394418)
ggplot(data = beer_ingredients_join, aes(total_hops, ibu)) +
geom_point(aes(total_hops, ibu, colour = style_collapsed)) +
geom_smooth(method = lm, se = FALSE, colour = "black") +
ggtitle("Hops Per Beer vs. Bitterness") +
labs(x = "Number of Hops", y = "IBU", colour = "Style Collapsed") +
theme_minimal()
hops_ibu_lm <- lm(ibu ~ total_hops, data = beer_ingredients_join)
summary(hops_ibu_lm)
- However, past a certain point (3 hops or more), there’s no effect of number of hops on IBU
ggplot(data = beer_ingredients_join[which(beer_ingredients_join$total_hops > 2
& beer_ingredients_join$total_hops < 8), ], aes(total_hops, ibu)) +
geom_point(aes(total_hops, ibu, colour = style_collapsed)) +
geom_smooth(method = lm, se = FALSE, colour = "black") +
ggtitle("3+ Hops Per Beer vs. Bitterness") +
labs(x = "Number of Hops", y = "IBU", colour = "Style Collapsed") +
theme_minimal()
Most popular hops
# Gather up all the hops columns into one called `hop_name`
beer_necessities_hops_gathered <- beer_necessities %>%
gather(
hop_key, hop_name, hops_name_1:hops_name_13
) %>% as_tibble()
# Filter to just those beers that have at least one hop
beer_necessities_w_hops <- beer_necessities_hops_gathered %>%
filter(!is.na(hop_name)) %>%
filter(!hop_name == "")
beer_necessities_w_hops$hop_name <- factor(beer_necessities_w_hops$hop_name)
# For all hops, find the number of beers they're in as well as those beers' mean IBU and ABV
hops_beer_stats <- beer_necessities_w_hops %>%
ungroup() %>%
group_by(hop_name) %>%
summarise(
mean_ibu = mean(ibu, na.rm = TRUE),
mean_abv = mean(abv, na.rm = TRUE),
n = n()
)
# Pare to hops that are used in at least 50 beers
pop_hops_beer_stats <- hops_beer_stats[hops_beer_stats$n > 50, ]
kable(pop_hops_beer_stats)
# Keep just beers that contain these most popular hops
beer_necessities_w_popular_hops <- beer_necessities_w_hops %>%
filter(hop_name %in% pop_hops_beer_stats$hop_name) %>%
droplevels()
Are there certian hops that are used more often in very high IBU or ABV beers? Hard to detect a pattern
ggplot(data = beer_necessities_w_popular_hops) +
geom_point(aes(abv, ibu, colour = hop_name)) +
ggtitle("Beers Containing most Popular Hops") +
labs(x = "ABV", y = "IBU", colour = "Hop Name") +
theme_minimal()
ggplot(data = pop_hops_beer_stats) +
geom_point(aes(mean_abv, mean_ibu, colour = hop_name, size = n)) +
ggtitle("Most Popular Hops' Effect on Alcohol and Bitterness") +
labs(x = "Mean ABV per Hop Type", y = "Mean IBU per Hop Type", colour = "Hop Name",
size = "Number of Beers") +
theme_minimal()
Neural Net
- Can ABV, IBU, and SRM be used in a neural net to predict
style or style_collapsed?
- In the function, specify the dataframe and the outcome, either
style or style_collapsed; the one not specified as outcome will be dropped
- The predictor columns will be everything not specified in the vector
predictor_vars
- The function returns the outcome variable sleected, neural net output, variable importance, the prediction dataframe, predictions, and accuracy
library(nnet)
library(caret)
run_neural_net <- function(df, outcome, predictor_vars) {
out <- list(outcome = outcome)
# Create a new column outcome; it's style_collapsed if you set outcome to style_collapsed, and style otherwise
if (outcome == "style_collapsed") {
df[["outcome"]] <- df[["style_collapsed"]]
} else {
df[["outcome"]] <- df[["style"]]
}
df$outcome <- factor(df$outcome)
cols_to_keep <- c("outcome", predictor_vars)
df <- df %>%
select_(.dots = cols_to_keep) %>%
mutate(row = 1:nrow(df)) %>%
droplevels()
# Select 80% of the data for training
df_train <- sample_n(df, nrow(df)*(0.8))
# The rest is for testing
df_test <- df %>%
filter(! (row %in% df_train$row)) %>%
select(-row)
df_train <- df_train %>%
select(-row)
# Build multinomail neural net
nn <- multinom(outcome ~ .,
data = df_train, maxit=500, trace=T)
# Which variables are the most important in the neural net?
most_important_vars <- varImp(nn)
# How accurate is the model? Compare predictions to outcomes from test data
nn_preds <- predict(nn, type="class", newdata = df_test)
nn_accuracy <- postResample(df_test$outcome, nn_preds)
out <- list(out, nn = nn, most_important_vars = most_important_vars,
df_test = df_test,
nn_preds = nn_preds,
nn_accuracy = nn_accuracy)
return(out)
}
- Set the dataframe to be
beer_totals, the predictor variables to be the vector contained in p_vars, the outcome to be style_collapsed
p_vars <- c("total_hops", "total_malt", "abv", "ibu", "srm", "glass")
nn_collapsed_out <- run_neural_net(df = beer_totals, outcome = "style_collapsed",
predictor_vars = p_vars)
Error in nnet.default(X, Y, w, mask = mask, size = 0, skip = TRUE, softmax = TRUE, :
too many (1422) weights
- What if we predcit
style instead of style_collapsed?
nn_notcollapsed_out <- run_neural_net(df = beer_totals, outcome = "style",
predictor_vars = p_vars)
nn_notcollapsed_out$nn_accuracy
nn_notcollapsed_out$most_important_vars
And now if we add glass as a predictor?
p_vars_add_glass <- c("total_hops", "total_malt", "abv", "ibu", "srm")
nn_collapsed_out_add_glass <- run_neural_net(df = beer_ingredients_join, outcome = "style_collapsed",
predictor_vars = p_vars_no_glass)
nn_collapsed_out_add_glass$nn_accuracy
nn_collapsed_out_add_glass$most_important_vars
Random forest with all ingredients
- We can use a random forest to get even more granular with ingredients
- The sparse ingredient dataframe was too complex for the multinomial neural net; however, we can
- Here we don’t include
glass as a predictor
library(ranger)
library(stringr)
bi <- beer_ingredients_join %>%
select(-c(id, name, cluster_assignment, style, hops_name, malt_name,
glass)) %>%
mutate(row = 1:nrow(.))
bi$style_collapsed <- factor(bi$style_collapsed)
# ranger complains about special characters and spaces in ingredient column names. Take them out and replace with empty string.
names(bi) <- tolower(names(bi))
names(bi) <- str_replace_all(names(bi), " ", "")
names(bi) <- str_replace_all(names(bi), "([\\(\\)-\\/')]+)", "")
# Keep 80% for training
bi_train <- sample_n(bi, nrow(bi)*(0.8))
# The rest is for testing
bi_test <- bi %>%
filter(! (row %in% bi_train$row)) %>%
dplyr::select(-row)
bi_train <- bi_train %>%
dplyr::select(-row)
bi_rf <- ranger(style_collapsed ~ ., data = bi_train, importance = "impurity", seed = 11)
OOB (out of bag) prediction error is around 58% * This calculated from tree samples constructed but not used in training set; these trees become effectively part of test set
bi_rf
We can compare predicted classification on the test set to their actual style classification.
pred_bi_rf <- predict(bi_rf, dat = bi_test)
table(bi_test$style_collapsed, pred_bi_rf$predictions)
Variable importance * Interestingly, ABV, IBU, and SRM are all much more important in the random forest than total_hops and total_malt
importance(bi_rf)[1:10]
How does a CSRF (case-specific random forest) fare?
bi_csrf <- csrf(style_collapsed ~ ., training_data = bi_train, test_data = bi_test,
params1 = list(num.trees = 5, mtry = 4),
params2 = list(num.trees = 2))
csrf_acc <- postResample(bi_csrf, bi_test$style_collapsed)
csrf_acc
Final Thoughts
Style first, forgiveness later?
- One reason seems that beers are generally brewed with style in mind first (“let’s make a pale ale”) rather than deciding the beer’s style after determining its characteristics and idiosyncrasies
- Even if the beer turns out more like a sour, and in a blind taste test might be classified as a sour more often than a pale ale, it still gets the label pale ale
- This makes the style definitions broader and harder to predict
Future Directions
- Incorporate flavor profiles for beers sourced/scraped from somewhere
- Implement a GAN to come up with beer names
- More on the hops deep dive: which hops are used most often in which styles?
LS0tCnRpdGxlOiBEYXRhIFNjaWVuY2UgTXVzaW5ncyBvbiBCZWVyCmF1dGhvcjoKICBuYW1lOiBBbWFuZGEgRG9iYnluCmRhdGU6ICdgciBmb3JtYXQoU3lzLnRpbWUoKSwgIiVCICVkLCAlWSIpYCcKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IGZhbHNlCiAgICB0aGVtZTogeWV0aQogIHBkZl9kb2N1bWVudDoKICAgIHRvYzogZmFsc2UKICBnaXRodWJfZG9jdW1lbnQ6CiAgICB0b2M6IHRydWUKICAgIAojIG91dHB1dDoKIyAgIGh0bWxfZG9jdW1lbnQ6CiMgICAgIGtlZXBfbWQ6IHRydWUKIyAgICAgdG9jOiBmYWxzZQojICAgICB0aGVtZTogeWV0aQojICAgZ2l0aHViX2RvY3VtZW50OgojICAgICB0b2M6IHRydWUKLS0tCgoKCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQpsaWJyYXJ5KGtuaXRyKQoKIyBrbml0cjo6b3B0c19rbml0JHNldChyb290LmRpcj1ub3JtYWxpemVQYXRoKCcuLi8nKSkKa25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFKQprbml0cjo6b3B0c19jaHVuayRzZXQoZmlnLndpZHRoPTEyLCBmaWcuaGVpZ2h0PTgpIApvcHRpb25zKGtuaXRyLnRhYmxlLmZvcm1hdCA9ICdtYXJrZG93bicpCgojIHNvdXJjZSgiLi9yZWFkX2Zyb21fZGIuUiIpCnNvdXJjZSgiLi9tb3N0X3BvcHVsYXJfc3R5bGVzLlIiKQojICMgc291cmNlKCIuL2NsdXN0ZXIuUiIpCiMgIyBzb3VyY2UoIi4vcGxvdC5SIikKIyBzb3VyY2UoIi4vZ2V0X2luZ3JlZGllbnRfbGV2ZWxzLlIiKQpgYGAKClRoaXMgaXMgYSBmaXJzdCBwYXNzIGV4cGxvcmF0aW9uIG9mIGRpZmZlcmVudCBhc3BlY3RzIG9mIGJlZXIuCgoqIERhdGEgY291cnRlc3kgb2YgW0JyZXdlcnlEQl0oaHR0cDovL3d3dy5icmV3ZXJ5ZGIuY29tL2RldmVsb3BlcnMpCiAgICAqIFNwZWNpYWwgdGhhbmtzIHRvIFtLcmlzIEtyb3NraV0oaHR0cHM6Ly9rcm8uc2tpLykgZm9yIGRhdGEgaWRlYXRpb24gYW5kIGJlZXIKICAgIAogICAgCioqKiAKCgoqIFRoZSBtYWluIHF1ZXN0aW9uIG9uIHRoZSB0YWJsZSBpcyB0aGlzOgogICAgKiBBcmUgYmVlciBzdHlsZXMgYWN0dWFsbHkgaW5kaWNhdGl2ZSBvZiBzaGFyZWQgYXR0cmlidXRlcyBvZiB0aGUgYmVlcnMgd2l0aGluIHRoYXQgc3R5bGU/IE9yIGFyZSBzdHlsZSBib3VuZGFyaWVzIG1vcmUgb3IgbGVzcyBhcmJpdHJhcnk/CiAgICAgICogVHdvIGFwcHJvYWNoZXM6IGNsdXN0ZXJpbmcgYW5kIHByZWRpY3Rpb24gCiAgICAgICogQ2x1c3RlcmluZzogYXJlIHRoZXJlIG5hdHVyYWwgY2x1c3RlcnMgYWNyb3NzIHRoZSBzcGVjdHVtIG9mIGJlZXJzIHRoYXQgYWxpZ24gd2VsbCB3aXRoIHRoZSBzdHlsZXMgdGhleSdyZSBncm91cGVkIGludG8/IAogICAgICAgICAgKiBVbnN1cGVydmlzZWQgKGstbWVhbnMpIGNsdXN0ZXJpbmcgYmFzZWQgb24gCiAgICAgICAgICAgICAgKiBBQlYgKGFsY29ob2wgYnkgdm9sdW1lKSwgSUJVIChpbnRlcm5hdGlvbmFsIGJpdHRlcm5lc3MgdW5pdHMpLCBTUk0gKG1lYXN1cmUgb2YgY29sb3IpCiAgICAgICAgICAgICAgKiBIb3cgd2VsbCBkbyB0aGVzZSBtYXRjaCB1cCB3aXRoIHZhcmlvdXMgInN0eWxlIGNlbnRlcnMsIiBkZWZpbmVkIGJ5IG1lYW4gb2YgQUJWLCBJQlUsIGFuZCBTUk0gcGVyIGJlZXIgc3R5bGUKICAgICAgKiBQcmVkaWN0aW9uOiBjYW4gd2UgcHJlZGljdCBhIGJlZXIncyBzdHlsZSBiYXNlZCBvbiBjZXJ0YWluIGNoYXJhY3RlcmlzdGljcyBvZiB0aGUgYmVlcj8KICAgICAgICAgICogTmV1cmFsIG5ldCAKICAgICAgICAgICogUmFuZG9tIGZvcmVzdAogICAgICAKKiBBbnN3ZXIgdGh1cyBmYXIKICAgICogQmVlci1pbnRyaW5zaWMgYXR0cmlidXRlcyBhcmVuJ3QgZ3JlYXQgcHJlZGljdG9ycyBvZiBzdHlsZQogICAgKiBTdHlsZS1kZWZpbmVkIGF0dHJpYnV0ZXMgYXJlIHRoZSBiZXN0IHByZWRpY3RvcnMKICAgICAgICAqIEZvciBpbnN0YW5jZSwgdGhlIGdsYXNzIGEgYmVlciBpcyBzZXJ2ZWQgaW4gKHdoaWNoIGlzIGRlZmluZWQgYnkgaXRzIHN0eWxlKSBpcyBhIG11Y2ggYmV0dGVyIHByZWRpY3RvciBvZiBpdHMgc3R5bGUgdGhhbiBhY3R1YWwgY2hhcmFjdGVyaXN0aWNzIG9mIHRoZSBiZWVyIGxpa2UgQUJWIGFuZCBldmVuIHRoZSBudW1iZXIgb2YgZGlmZmVyZW50IHR5cGVzIG9mIGhvcHMgaXQgY29udGFpbnMKCiFbXSguL3RhcHMuanBnKQoKCgojIyMgV29ya2Zsb3cgT3ZlcnZpZXcKCiogSGl0IHRoZSBCcmV3ZXJ5REIgQVBJIHRvIGl0ZXJhdGl2ZWx5IHB1bGwgaW4gYWxsIGJlZXJzIGFuZCB0aGVpciBpbmdyZWRpZW50cyBhbG9uZyB3aXRoIG90aGVyIHRoaW5ncyB3ZSBtaWdodCB3YW50IGxpa2UgYnJld2VyaWVzIGFuZCBnbGFzc3dhcmUKKiBVbm5lc3QgdGhlIEpTT04gcmVzcG9uc2VzLCBpbmNsdWRpbmcgYWxsIHRoZSBpbmdyZWRpZW50cyBjb2x1bW5zLCBhbmQgCiogRHVtcCB0aGlzIGFsbCBpbnRvIGEgTXlTUUwgZGF0YWJhc2UgCgoqIENyZWF0ZSBhIGBzdHlsZV9jb2xsYXBzZWRgIGNvbHVtbiB0byByZWR1Y2UgdGhlIG51bWJlciBvZiBsZXZlbHMgb2Ygb3VyIG91dGNvbWUgdmFyaWFibGUKICAgICogYGdyZXBgIHRocm91Z2ggZWFjaCBiZWVyJ3Mgc3R5bGUgdG8gZGV0ZXJtaW5lIGlmIHRoYXQgc3R5bGUgY29udGFpbnMgYSBrZXl3b3JkIHRoYXQgcXVhbGlmaWVzIGl0IHRvIGJlIHJvbGxlZCBpbnRvIGEgY29sbGFwc2VkIHN0eWxlCiAgICAqIElmIGl0IGRvZXMsIGl0IGdldHMgdGhhdCBrZXl3b3JkIGluIGEgYHN0eWxlX2NvbGxhcHNlZGAgY29sdW1uIAogICAgKiBGdXJ0aGVyIGNvbGxwYXNlIHN0eWxlcyB0aGF0IGFyZSBzaW1pbGFyIGxpa2UgSGVmZXdlaXplbiBhbmQgV2l0IGludG8gV2hlYXQKICAgIAoqIFVubmVzdCB0aGUgaW5ncmVkaWVudHMgYGhvcHNgIGFuZCBgbWFsdHNgIGludG8gYSBzcGFyc2UgbWF0cml4CiAgICAqIEluZGl2aWR1YWwgaW5ncmVkaWVudHMgYXMgY29sdW1ucywgYmVlcnMgYXMgcm93czsgY2VsbCBnZXRzIGEgMSBpZiBpbmdyZWRpZW50IGlzIHByZXNlbnQgYW5kIDAgb3RoZXJ3aXNlIAogICAgCiogQ2x1c3RlcjogdW5zdXBlcnZpc2VkIGstbWVhbnMgY2xzdXRlcmluZyBiYXNlZCBvbiBBQlYsIElCVSwgYW5kIFNSTQoKKiBSdW4gYSBuZXVyYWwgbmV0CiAgICAqIFByZWRpY3QgZWl0aGVyIGBzdHlsZWAgb3IgYHN0eWxlX2NvbGxhcHNlZGAgZnJvbSBhbGwgdGhlIHByZWRpY3RvcnMgaW5jbHVkaW5nIHRoZSB0b3RhbCBudW1iZXIgb2YgaG9wcyBhbmQgbWFsdHMgcGVyIGJlZXIKCgoqKlNob3J0IEFzaWRlKioKClRoZSBxdWVzdGlvbiBvZiB3aGF0IHNob3VsZCBiZSBhIHByZWRpY3RvciB2YXJpYWJsZSBmb3Igc3R5bGUgaXMgYSBiaXQgbXVya3kgaGVyZS4gV2hhdCBzaG91bGQgYmUgZmFpciBnYW1lIGZvciBwcmVkaWN0aW5nIHN0eWxlIGFuZCB3aGF0IHNob3VsZG4ndD8gQ2hhcmFjdGVyaXN0aWNzIG9mIGEgYmVlciB0aGF0IGFyZSBkZWZpbmVkICpieSogaXRzIHN0eWxlIHdvdWxkIHNlZW0gdG8gYmUgImNoZWF0aW5nIiBpbiBhIHdheS4gCgoqIE1haW4gY2FuZGlkYXRlcyBhcmU6CiAgICAqIEFCViAoYWxjb2hvbCBieSB2b2x1bWUpLCBJQlUgKGludGVybmF0aW9uYWwgYml0dGVybmVzcyB1bml0cyksIFNSTSAoc3RhbmRhcmQgcmVmZXJlbmNlIG1lYXN1cmUsIGEgc2NhbGUgb2YgYmVlciBjb2xvciBmcm9tIGxpZ2h0IHRvIGRhcmspCiAgICAgICAgKiBUaGVzZSBhcmUgb3V0cHV0cyBvZiBhIGJlZXIgdGhhdCBtZWFuaW5nZnVsbHkgZGVmaW5lIHRoZSBiZWVyIGFuZCBhcmUgdGhlb3JldGljYWxseSBvcnRob2dvbmFsIHRvIGVhY2ggb3RoZXIKICAgICogSW5ncmVkaWVudHMgaW4gYSBiZWVyIHN1Y2ggYXMgaG9wcyBhbmQgbWFsdHMKICAgICAgICAqIElucHV0cyB0byBhIGJlZXIgdGhhdCBoYXZlIHNvbWUgZWZmZWN0IG9uIGl0cyBmbGF2b3IgcHJvZmlsZQogICAgICAgICogU2VtaS1jaGVhdGluZyBiZWNhdXNlIGlmIHN0eWxlIGlzIGRldGVybWluZWQgYmVmb3JlaGFuZCBpdCBsaWtlbHkgZGV0ZXJtaW5lcyBhdCBsZWFzdCBpbiBwYXJ0IHdoaWNoIGluZ3JlZGllbnRzIGFyZSBhZGRlZCAKICAgICogR2xhc3MgdHlwZQogICAgICAgICogVGhpcyBpcyBkZWZpbmVkIGVudGlyZWx5IGJ5IHN0eWxlIGFuZCBpcyB2ZXJ5IHByZWRpY3RpdmUgb2YgaXQKCgoKKipUaGlzIGRvY3VtZW50IGNvbXBpbGVkIGJ5IHF1ZXJ5aW5nIHRoZSBiZWVyIGRhdGFiYXNlIEkgYnVpbHQsIHNwZWNpZmljYWxseSBieSBzb3VyY2luZyB0aGUgZmlsZSByZWFkX2Zyb21fZGIuUi4gVGhpcyBpcyBkb25lIGZvciBleHBlZGllbmN5J3Mgc2FrZSwgKHRoZSBjb2RlIGJlbG93IGRldGFpbGluZyBob3cgdG8gZ2V0IHRoZSBiZWVyLCBydW4gaW4gZnVsbCBpbiBydW5faXQuUiwgdGFrZXMgc29tZSB0aW1lIHRvIGV4ZWN1dGUpLioqIAoKCiMjIyBHZXQgYW5kIFByZXBhcmUgRGF0YQoKKipHZXR0aW5nIGJlZXIsIHRoZSBhZ2Utb2xkIGRpbGVtbWEqKgoKKiBUaGUgQnJld2VyeURCIEFQSSByZXR1cm5zIGEgY2VydGFpbiBudW1iZXIgb2YgcmVzdWx0cyBwZXIgcGFnZTsgaWYgd2Ugd2FudCAKKiBTbywgd2UgaGl0IHRoZSBCcmV3ZXJ5REIgQVBJIGFuZCBhc2sgZm9yIGAxOm51bWJlcl9vZl9wYWdlc2AKICAgICogV2UgY2FuIGNoYW5nZSBgbnVtYmVyX29mX3BhZ2VzYCB0bywgZS5nLiwgMyBpZiB3ZSBvbmx5IHdhbnQgdGhlIGZpcnN0IDMgcGFnZXMKICAgICogSWYgdGhlcmUncyBvbmx5IG9uZSBwYWdlIChhcyBpcyB0aGUgY2FzZSBmb3IgdGhlIGdsYXNzd2FyZSBlbmRwb2luZyksIG51bWJlck9mUGFnZXMgd29uJ3QgYmUgcmV0dXJuZWQsIHNvIGluIHRoaXMgY2FzZSB3ZSBzZXQgbnVtYmVyX29mX3BhZ2VzIHRvIDEKKiBUaGUgYGFkZGl0aW9uYCBwYXJhbWV0ZXIgY2FuIGJlIGFuIGVtcHR5IHN0cmluZyBpZiBub3RoaW5nIGVsc2UgaXMgbmVlZGVkCgpgYGB7ciwgZXZhbCA9IEZBTFNFLCBlY2hvPVRSVUV9CgpiYXNlX3VybCA8LSAiaHR0cDovL2FwaS5icmV3ZXJ5ZGIuY29tL3YyIgprZXlfcHJlZmFjZSA8LSAiLz9rZXk9IgoKcGFnaW5hdGVkX3JlcXVlc3QgPC0gZnVuY3Rpb24oZXAsIGFkZGl0aW9uKSB7ICAgIAogIGZ1bGxfcmVxdWVzdCA8LSBOVUxMCiAgZmlyc3RfcGFnZSA8LSBmcm9tSlNPTihwYXN0ZTAoYmFzZV91cmwsICIvIiwgZXAsICIvIiwga2V5X3ByZWZhY2UsIGtleQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICwgIiZwPTEiKSkKICBudW1iZXJfb2ZfcGFnZXMgPC0gaWZlbHNlKCEoaXMubnVsbChmaXJzdF9wYWdlJG51bWJlck9mUGFnZXMpKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBmaXJzdF9wYWdlJG51bWJlck9mUGFnZXMsIDEpICAgICAgCgogICAgZm9yIChwYWdlIGluIDE6bnVtYmVyX29mX3BhZ2VzKSB7ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgdGhpc19yZXF1ZXN0IDwtIGZyb21KU09OKHBhc3RlMChiYXNlX3VybCwgIi8iLCBlcCwgIi8iLCBrZXlfcHJlZmFjZSwga2V5CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICwgIiZwPSIsIHBhZ2UsIGFkZGl0aW9uKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBmbGF0dGVuID0gVFJVRSkgCiAgICB0aGlzX3JlcV91bm5lc3RlZCA8LSB1bm5lc3RfaXQodGhpc19yZXF1ZXN0KSAgICAjICA8LSByZXF1ZXN0IHVubmVzdGVkIGhlcmUKICAgIHByaW50KHRoaXNfcmVxX3VubmVzdGVkJGN1cnJlbnRQYWdlKQogICAgZnVsbF9yZXF1ZXN0IDwtIGJpbmRfcm93cyhmdWxsX3JlcXVlc3QsIHRoaXNfcmVxX3VubmVzdGVkW1siZGF0YSJdXSkKICB9CiAgZnVsbF9yZXF1ZXN0Cn0gCgphbGxfYmVlcl9yYXcgPC0gcGFnaW5hdGVkX3JlcXVlc3QoImJlZXJzIiwgIiZ3aXRoSW5ncmVkaWVudHM9WSIpCmBgYAoKCgoqIEZ1bmN0aW9uIGZvciB1bm5lc3RpbmcgSlNPTiB1c2VkIGluc2lkZSBgcGFnaW5hdGVkX3JlcXVlc3QoKWAgYmVsb3cKICAgICsgVGFrZXMgdGhlIGNvbHVtbiBuYW1lZCBgbmFtZWAgbmVzdGVkIHdpdGhpbiBhIGNvbHVtbiBpbiB0aGUgZGF0YSBwb3J0aW9uIG9mIHRoZSByZXNwb25zZQogICAgICAgICsgSWYgdGhlIGBuYW1lYCBjb2x1bW4gZG9lc24ndCBleGlzdCwgaXQgdGFrZXMgdGhlIGZpcnN0IG5lc3RlZCBjb2x1bW4KKiBXZSB1c2Ugc29tZXRoaW5nIHNpbWlsYXIgdG8gdW5uZXN0IGluZ3JlZGllbnQgbGlrZSBhbGwgb2YgYSBiZWVyJ3MgaG9wcyBhbmQgbWFsdHMgaW50byBhIGxvbmcgc3RyaW5nIGNvbnRhaW5lZCBpbiBgaG9wc19uYW1lYCBhbmQgYG1hbHRfbmFtZWAKCmBgYHtyLCBldmFsPUZBTFNFLCBlY2hvPVRSVUV9CnVubmVzdF9pdCA8LSBmdW5jdGlvbihkZikgewogIHVubmVzdGVkIDwtIGRmCiAgZm9yKGNvbCBpbiBzZXFfYWxvbmcoZGZbWyJkYXRhIl1dKSkgewogICAgaWYoISBpcy5udWxsKG5jb2woZGZbWyJkYXRhIl1dW1tjb2xdXSkpKSB7CiAgICAgIGlmKCEgaXMubnVsbChkZltbImRhdGEiXV1bW2NvbF1dW1sibmFtZSJdXSkpIHsKICAgICAgICB1bm5lc3RlZFtbImRhdGEiXV1bW2NvbF1dIDwtIGRmW1siZGF0YSJdXVtbY29sXV1bWyJuYW1lIl1dCiAgICAgIH0gZWxzZSB7CiAgICAgICAgdW5uZXN0ZWRbWyJkYXRhIl1dW1tjb2xdXSA8LSBkZltbImRhdGEiXV1bW2NvbF1dW1sxXV0KICAgICAgfQogICAgfQogIH0KICB1bm5lc3RlZAp9CmBgYAoKCgoqKkNvbGxhcHNlIFN0eWxlcyoqCgoqIFNhdmUgdGhlIG1vc3QgcG9wdWxhciBzdHlsZXMgaW4gYGtleXdvcmRzYAoqIExvb3AgdGhyb3VnaCBlYWNoIGtleXdvcmQKICAgICogRm9yIGVhY2ggYmVlciwgYGdyZXBgIHRocm91Z2ggaXRzIHN0eWxlIGNvbHVtbiB0byBzZWUgaWYgaXQgY29udGFpbnMgYW55IG9uZSBvZiB0aGVzZSBrZXl3b3JkcwogICAgKiBJZiBpdCBkb2VzLCBnaXZlIGl0IHRoYXQga2V5d29yZCBpbiBhIG5ldyBjb2x1bW4gYHN0eWxlX2NvbGxhcHNlZGAKKiBJZiBhIGJlZXIncyBuYW1lIG1hdGNoZXMgbXVsdGlwbGUga2V5d29yZHMsIGUuZy4sIEFtZXJpY2FuIERvdWJsZSBJbmRpYSBQYWxlIEFsZSB3b3VsZCBtYXRjaCBEb3VibGUgSW5kaWEgUGFsZSBBbGUsIEluZGlhIFBhbGUgQWxlLCBhbmQgUGFsZSBBbGUsIGl0cyBgc3R5bGVfY29sbGFwc2VkYCBpcyB0aGUgKipsYXN0Kiogb2YgdGhvc2UgdGhhdCBhcHBlYXIgaW4ga2V5d29yZHMgCiAgICAqIFRoaXMgaXMgd2h5IGtleXdvcmRzIGFyZSBpbnRlbnRpb25hbGx5IG9yZGVyZWQgZnJvbSBtb3N0IGdlbmVyYWwgdG8gbW9zdCBzcGVjaWZpYwogICAgKiBTbyBpbiB0aGUgY2FzZSBvZiBhbiBjYXNlIG9mIEFtZXJpY2FuIERvdWJsZSBJbmRpYSBQYWxlIEFsZTogc2luY2UgRG91YmxlIEluZGlhIFBhbGUgQWxlIGFwcGVhcnMgaW4gYGtleXdvcmRzYCBhZnRlciBJbmRpYSBQYWxlIEFsZSBhbmQgUGFsZSBBbGUsIGFuIEFtZXJpY2FuIERvdWJsZSBJbmRpYSBQYWxlIEFsZSB3b3VsZCBnZXQgYSBgc3R5bGVfY29sbGFwc2VkYCBvZiBEb3VibGUgSW5kaWEgUGFsZSBBbGUKKiBJZiBubyBrZXl3b3JkIGlzIGNvbnRhaW5lZCBpbiBgc3R5bGVgLCBgc3R5bGVfY29sbGFwc2VkYCBpcyBqdXN0IHdoYXRldmVyJ3MgaW4gYHN0eWxlYDsgaW4gb3RoZXIgd29yZHMsIGl0IGRvZXNuJ3QgZ2V0IGNvbGxwc2VkIGludG8gYSBiaWdnZXIgYnVja2V0CiAgICAqIFRoaXMgaXNuJ3QgYSBodWdlIHByb2JsZW0gYmVjYXVzZSB3ZSdsbCBwYXJlIGRvd24gdG8ganVzdCB0aGUgbW9zdCBwb3B1bGFyIHN0eWxlcyBsYXRlciwgaG93ZXZlciB3ZSBjb3VsZCB0aGluayBhYm91dCBjcmVhdGluZyBhIGNhdGNoYWxsICJPdGhlciIgbGV2ZWwgZm9yIGBzdHlsZV9jb2xsYXBzZWRgCgpgYGB7ciwgZXZhbD1GQUxTRSwgZWNobz1UUlVFfQoKY29sbGFwc2Vfc3R5bGVzIDwtIGZ1bmN0aW9uKGRmKSB7CiAga2V5d29yZHMgPC0gYygiTGFnZXIiLCAiUGFsZSBBbGUiLCAiSW5kaWEgUGFsZSBBbGUiLCAiRG91YmxlIEluZGlhIFBhbGUgQWxlIiwgIkluZGlhIFBhbGUgTGFnZXIiLCAiSGVmZXdlaXplbiIsICJCYXJyZWwtQWdlZCIsIldoZWF0IiwgIlBpbHNuZXIiLCAiUGlsc2VuZXIiLCAiQW1iZXIiLCAiR29sZGVuIiwgIkJsb25kZSIsICJCcm93biIsICJCbGFjayIsICJTdG91dCIsICJQb3J0ZXIiLCAiUmVkIiwgIlNvdXIiLCAiS8O2bHNjaCIsICJUcmlwZWwiLCAiQml0dGVyIiwgIlNhaXNvbiIsICJTdHJvbmcgQWxlIiwgIkJhcmxleSBXaW5lIiwgIkR1YmJlbCIsICJBbHRiaWVyIikKICAKICBmb3IgKGJlZXIgaW4gMTpucm93KGRmKSkgewogICAgaWYgKGdyZXBsKHBhc3RlKGtleXdvcmRzLCBjb2xsYXBzZT0ifCIpLCBkZiRzdHlsZVtiZWVyXSkpIHsgICAgCiAgICAgIGZvciAoa2V5d29yZCBpbiBrZXl3b3JkcykgeyAgICAgICAgIAogICAgICAgIGlmKGdyZXBsKGtleXdvcmQsIGRmJHN0eWxlW2JlZXJdKSA9PSBUUlVFKSB7CiAgICAgICAgICBkZiRzdHlsZV9jb2xsYXBzZWRbYmVlcl0gPC0ga2V5d29yZCAgICAKICAgICAgICB9ICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICB9IAogICAgfSBlbHNlIHsKICAgICAgZGYkc3R5bGVfY29sbGFwc2VkW2JlZXJdIDwtIGFzLmNoYXJhY3RlcihkZiRzdHlsZVtiZWVyXSkgICAgICAgCiAgICB9CiAgICBwcmludChkZiRzdHlsZV9jb2xsYXBzZWRbYmVlcl0pCiAgfQogIHJldHVybihkZikKfQoKYGBgCgoqIFRoZW4gd2UgY29sbGFwc2UgZnVydGhlcjsgcmlnaHQgbm93IHdlIGp1c3QgY29tYmluZSBhbGwgd2hlYXR5IGJlYXJzIGludG8gV2hlYXQgYW5kIFBpbHMtbGlrZSBiZWVycyBpbnRvIFBpbHNlbmVyICh3aXRoIHR3byBlJ3MpIGJ5IGBmY3RfY29sbGFwc2VgaW5nIHRob3NlIGxldmVscwoKYGBge3IsIGVjaG89VFJVRSwgZXZhbD1GQUxTRX0KY29sbGFwc2VfZnVydGhlciA8LSBmdW5jdGlvbihkZikgewogIGRmW1sic3R5bGVfY29sbGFwc2VkIl1dIDwtIGRmW1sic3R5bGVfY29sbGFwc2VkIl1dICU+JQogICAgZmN0X2NvbGxhcHNlKAogICAgICAiV2hlYXQiID0gYygiSGVmZXdlaXplbiIsICJXaGVhdCIpLAogICAgICAiUGlsc2VuZXIiID0gYygiUGlsc25lciIsICJBbWVyaWNhbi1TdHlsZSBQaWxzZW5lciIpICMgcGlsc2VuZXIgPT0gcGlsc25lciA9PSBwaWxzCiAgICApCiAgcmV0dXJuKGRmKQp9CmBgYAoKCgoqKlNwbGl0IG91dCBJbmdyZWRpZW50cyoqCgoqIFdoZW4gd2UgdW5uZXN0ZWQgaW5ncmVkaWVudHMsIHdlIGp1c3QgY29uY2F0ZW5hdGVkIGFsbCBvZiB0aGUgaW5ncmVkaWVudHMgZm9yIGEgZ2l2ZW4gYmVlciBpbnRvIGEgbG9uZyBzdHJpbmcKKiBJZiB3ZSB3YW50LCB3ZSBjYW4gc3BsaXQgb3V0IHRoZSBpbmdyZWRpZW50cyB0aGF0IHdlcmUgY29uY2F0ZW5hdGVkIGluIGA8aW5ncmVkaWVudD5fbmFtZWAgd2l0aCB0aGlzIGBzcGxpdF9pbmdyZWRpZW50c2AgZnVuY3Rpb24KKiBUaGlzIHRha2VzIGEgdmVjdG9yIG9mIGBpbmdyZWRpZW50c190b19zcGxpdGAsIHNvIGUuZy4gYGMoImhvcHNfbmFtZSIsICJtYWx0X25hbWUiKWAgYW5kIGNyZWF0ZXMgb25lIGNvbHVtbiBmb3IgZWFjaCB0eXBlIG9mIGluZ3JlZGllbnQgKGBob3BzX25hbWVfMWAsIGBob3BzX25hbWVfMmAsIGV0Yy4pCgoqIFdlIGBzdHJfc3BsaXRgIG9uIHRoZSBpbmdyZWRpZW50IGFuZCBnZXQgYSBsaXN0IGJhY2sKKiBXZSBmaW5kIHRoZSBtYXggbnVtYmVyIG9mIGluc3RhbmNlcyBvZiBhbiBpbmdyZWRpZW50IHBlciBiZWVyLCB3aGljaCB3aWxsIGJlIHRoZSBudW1iZXIgb2YgY29sdW1ucyB3ZSdyZSBhZGRpbmcKKiBGb3IgZWFjaCBuZXcgY29sdW1uIHdlIG5lZWQsIHdlIGNyZWF0ZSBpdCwgaW5pdGlhbGl6ZSBpdCB3aXRoIE5BcywgYW5kIG5hbWUgaXQKKiBUaGVuIGZvciBlYWNoIGVsZW1lbnQgaW4gb3VyIGxpc3Qgb2Ygc3BsaXQgdXAgaW5ncmVkaWVudHMsIGlmIGl0IGV4aXN0cywgd2UgYWRkIGl0IHRvIHRoZSBjb3JyZWN0IGNvbHVtbiBpbiBvdXIgZGYKCmBgYHtyLCBldmFsPUZBTFNFLCBlY2hvPVRSVUV9CnNwbGl0X2luZ3JlZGllbnRzIDwtIGZ1bmN0aW9uKGRmLCBpbmdyZWRpZW50c190b19zcGxpdCkgewogIAogIG5jb2xfZGYgPC0gbmNvbChkZikKICAKICBmb3IgKGluZ3JlZGllbnQgaW4gaW5ncmVkaWVudHNfdG9fc3BsaXQpIHsKCiAgICBpbmdyZWRpZW50X3NwbGl0IDwtIHN0cl9zcGxpdChkZltbaW5ncmVkaWVudF1dLCAiLCAiKSAgICAKICAgIG51bV9uZXdfY29scyA8LSBtYXgobGVuZ3RocyhpbmdyZWRpZW50X3NwbGl0KSkgICAgCiAgCiAgICBmb3IgKG51bSBpbiAxOm51bV9uZXdfY29scykgewogICAgICAKICAgICAgdGhpc19jb2wgPC0gbmNvbF9kZiArIDEgICAgICAgICAKICAgICAgCiAgICAgIGRmWywgdGhpc19jb2xdIDwtIE5BCiAgICAgIG5hbWVzKGRmKVt0aGlzX2NvbF0gPC0gcGFzdGUwKGluZ3JlZGllbnQsICJfIiwgbnVtKQogICAgICBuY29sX2RmIDwtIG5jb2woZGYpICAgICAgICAgICAgIAogICAgICBmb3IgKHJvdyBpbiBzZXFfYWxvbmcoaW5ncmVkaWVudF9zcGxpdCkpIHsgICAgICAgICAgCiAgICAgICAgaWYgKCFpcy5udWxsKGluZ3JlZGllbnRfc3BsaXRbW3Jvd11dW251bV0pKSB7ICAgICAgICAKICAgICAgICAgIGRmW3JvdywgdGhpc19jb2xdIDwtIGluZ3JlZGllbnRfc3BsaXRbW3Jvd11dW251bV0KICAgICAgICB9CiAgICAgIH0KICAgICAgZGZbW25hbWVzKGRmKVt0aGlzX2NvbF1dXSA8LSBmYWN0b3IoZGZbW25hbWVzKGRmKVt0aGlzX2NvbF1dXSkKICAgIH0KICAgIAogICAgbmNvbF9kZiA8LSBuY29sKGRmKQogIH0KICByZXR1cm4oZGYpCn0KYGBgCgoKCioqRmluZCB0aGUgTW9zdCBQb3B1YWxhciBTdHlsZXMqKgoKKiBGaW5kIG1lYW4gQUJWLCBJQlUsIGFuZCBTUk0gcGVyIGNvbGxhcHNlZCBzdHlsZQoqIEFycmFuZ2UgY29sbGFwc2VkIHN0eWxlcyBieSB0aGUgbnVtYmVyIG9mIGJlZXJzIHRoYXQgZmFsbCBpbnRvIHRoZW0KICAgICogVGhpcyBpcyBvZiBjb3Vyc2UgZGVwZW5kZW50IG9uIGhvdyB3ZSBjb2xsYXBzZSBzdHlsZXMKCmBgYHtyLCBldmFsPUZBTFNFLCBlY2hvPVRSVUV9CmxpYnJhcnkoZm9yY2F0cykKCiMgUGFyZSBkb3duIHRvIG9ubHkgY2FzZXMgd2hlcmUgc3R5bGUgaXMgbm90IE5BCmJlZXJfZGF0IDwtIGJlZXJfbmVjZXNzaXRpZXMKCmJlZXJfZGF0X3BhcmVkIDwtIGJlZXJfZGF0W2NvbXBsZXRlLmNhc2VzKGJlZXJfZGF0JHN0eWxlKSwgXQoKIyBBcnJhbmdlIGJlZXIgZGF0IGJ5IHN0eWxlIHBvcHVsYXJpdHkKc3R5bGVfcG9wdWxhcml0eSA8LSBiZWVyX2RhdF9wYXJlZCAlPiUgCiAgZ3JvdXBfYnkoc3R5bGUpICU+JSAKICBjb3VudCgpICU+JSAKICBhcnJhbmdlKGRlc2MobikpCnN0eWxlX3BvcHVsYXJpdHkKCiMgQWRkIGEgY29sdW1uIHRoYXQgc2NhbGVzIHBvcHVsYXJpdHkKc3R5bGVfcG9wdWxhcml0eSA8LSBiaW5kX2NvbHMoc3R5bGVfcG9wdWxhcml0eSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX3NjYWxlZCA9IGFzLnZlY3RvcihzY2FsZShzdHlsZV9wb3B1bGFyaXR5JG4pKSkKCiMgRmluZCBzdHlsZXMgdGhhdCBhcmUgYWJvdmUgYSB6LXNjb3JlIG9mIDAKcG9wdWxhcl9zdHlsZXMgPC0gc3R5bGVfcG9wdWxhcml0eSAlPiUgCiAgZmlsdGVyKG5fc2NhbGVkID4gMCkKCiMgUGFyZSBkYXQgZG93biB0byBvbmx5IGJlZXJzIHRoYXQgZmFsbCBpbnRvIHRob3NlIHN0eWxlcwpwb3B1bGFyX2JlZXJfZGF0IDwtIGJlZXJfZGF0X3BhcmVkICU+JSAKICBmaWx0ZXIoCiAgICBzdHlsZSAlaW4lIHBvcHVsYXJfc3R5bGVzJHN0eWxlCiAgKSAlPiUgCiAgZHJvcGxldmVscygpICU+JSAKICBhc190aWJibGUoKSAKbnJvdyhwb3B1bGFyX2JlZXJfZGF0KQoKIyBGaW5kIHRoZSBjZW50ZXJzIChtZWFuIGFidiwgaWJ1LCBzcm0pIG9mIHRoZSBtb3N0IHBvcHVsYXIgc3R5bGVzCnN0eWxlX2NlbnRlcnMgPC0gcG9wdWxhcl9iZWVyX2RhdCAlPiUgCiAgZ3JvdXBfYnkoc3R5bGVfY29sbGFwc2VkKSAlPiUgCiAgYWRkX2NvdW50KCkgJT4lIAogIHN1bW1hcmlzZSgKICAgIG1lYW5fYWJ2ID0gbWVhbihhYnYsIG5hLnJtID0gVFJVRSksCiAgICBtZWFuX2lidSA9IG1lYW4oaWJ1LCBuYS5ybSA9IFRSVUUpLCAKICAgIG1lYW5fc3JtID0gbWVhbihzcm0sIG5hLnJtID0gVFJVRSksCiAgICBuID0gbWVkaWFuKG4sIG5hLnJtID0gVFJVRSkgICAgICAgICAgIyBNZWRpYW4gaGVyZSBvbmx5IGZvciBzdW1tYXJpc2UuIFNob3VsZCBiZSBqdXN0IHRoZSBzYW1lIGFzIG4KICApICU+JSAKICBhcnJhbmdlKGRlc2MobikpICU+JSAKICBkcm9wX25hKCkgJT4lIAogIGRyb3BsZXZlbHMoKQoKIyBHaXZlIHNvbWUgbmljZXIgbmFtZXMKc3R5bGVfY2VudGVyc19yZW5hbWUgPC0gc3R5bGVfY2VudGVycyAlPiUgCiAgcmVuYW1lKAogICAgYENvbGxhcHNlZCBTdHlsZWAgPSBzdHlsZV9jb2xsYXBzZWQsCiAgICBgTWVhbiBBQlZgID0gbWVhbl9hYnYsCiAgICBgTWVhbiBJQlVgID0gbWVhbl9pYnUsCiAgICBgTWVhbiBTUk1gID0gbWVhbl9zcm0sCiAgICBgTnVtZXIgb2YgQmVlcnNgID0gbgogICkKYGBgCgoKVGFrZSBhIGxvb2sgYXQgdGhlIHRhYmxlICAgICAgCgpgYGB7cn0Ka2FibGUoc3R5bGVfY2VudGVyc19yZW5hbWUpCmBgYAoKCioqKgoKTm93IHRoYXQgdGhlIG11bmdpbmcgaXMgZG9uZSwgb250byB0aGUgbWFpbiBxdWVzdGlvbjogZG8gbmF0dXJhbCBjbHVzdGVycyBpbiBiZWVyIGFsaWduIHdpdGggc3R5bGUgYm91bmRhcmllcz8KCgoKCioqKgoKIyMjIEluZ3JlZGllbnRzCgpUbyBnZXQgbW9yZSBncmFudWxhciB3aXRoIGluZ3JlZGllbnRzLCB3ZSBjYW4gc3BsaXQgb3V0IGVhY2ggaW5kaXZpZHVhbCBpbmdyZWRpZW50IGludG8gaXRzIG93biBjb2x1bW4uIElmIGEgYmVlciBvciBzdHlsZSBjb250YWlucyB0aGF0IGluZ3JlZGllbnQsIGl0cyByb3cgZ2V0cyBhIDEgaW4gdGhhdCBpbmdyZWRpZW50IGNvbHVtbiBhbmQgYSAwIG90aGVyd2lzZS4KCkZyb20gdGhpcywgd2UgY2FuIGZpbmQgdGhlIHRvdGFsIG51bWJlciBvZiBob3BzIGFuZCBtYWx0cyBwZXIgZ3JvdXBlci4KCiogVGhlIGRhdGFmcmFtZSB3ZSdsbCB1c2Ugd2lsbCBiZSBgYmVlcl9uZWNlc3NpdGllc2AKCgo8IS0tIGBgYHtyLCBldmFsPVRSVUUsIGVjaG89VFJVRX0gLS0+CjwhLS0gY2x1c3RlcmVkX2JlZXJfbmVjZXNzaXRpZXMgPC0gY2x1c3RlcmVkX2JlZXIgJT4lIC0tPgo8IS0tICAgaW5uZXJfam9pbihiZWVyX25lY2Vzc2l0aWVzKSAtLT4KCjwhLS0gYGBgIC0tPgoKKiBUaGlzIGZ1bmN0aW9uIHRha2VzIGEgZGF0YWZyYW1lIGFuZCB0d28gb3RoZXIgcGFyYW1ldGVycyBzZXQgYXQgdGhlIG91dHNldDoKICAgICogYGluZ3JlZGllbnRfd2FudGA6IHRoaXMgY2FuIGJlIGBob3BzYCwgYG1hbHRgLCBvciBvdGhlciBpbmdyZWRpZW50cyBsaWtlIGB5ZWFzdGAgaWYgd2UgcHVsbCB0aGF0IGluCiAgICAqIGBncm91cGVyYDogY2FuIGJlIGEgdmVjdG9yIG9mIG9uZSBvciBtb3JlIHRoaW5ncyB0byBncm91cCBieSwgbGlrZSBiZWVyIGBuYW1lYCBvciBgc3R5bGVgCgpgYGB7ciwgZXZhbD1UUlVFLCBlY2hvPVRSVUV9CgpwaWNrX2luZ3JlZGllbnRfZ2V0X2JlZXIgPC0gZnVuY3Rpb24gKGluZ3JlZGllbnRfd2FudCwgZGYsIGdyb3VwZXIpIHsKICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIFNldHVwIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjCiAgIyBXZSd2ZSBhbHJlYWR5IHNwbGl0IGluZ3JlZGllbnQgbnVtYmVyIG5hbWVzIG91dCBmcm9tIHRoZSBjb25jYXRlbmF0ZWQgc3RyaW5nIGludG8gY29sdW1ucyBsaWtlIGBtYWx0X25hbWVfMWAsCiAgIyBgbWFsdF9uYW1lXzJgLCBldGMuIFdlIG5lZWQgdG8gZmluZCB0aGUgcmFuZ2Ugb2YgdGhlc2UgY29sdW1uczsgdGhlcmUgd2lsbCBiZSBhIGRpZmZlcmVudCBudW1iZXIgb2YgbWFsdAogICMgY29sdW1ucyB0aGFuIGhvcHMgY29sdW1ucywgZm9yIGluc3RhbmNlLiBUaGUgZmlyc3Qgb25lIHdpbGwgYmUgYDxpbmdyZWRpZW50Pl9uYW1lXzFgIGFuZCBmcm9tIHRoaXMgd2UgY2FuIGZpbmQKICAjIHRoZSBpbmRleCBvZiB0aGlzIGNvbHVtbiBpbiBvdXIgZGF0YWZyYW1lLiBXZSBnZXQgdGhlIG5hbWUgb2YgbGFzdCBvbmUgd2l0aCB0aGUgYGdldF9sYXN0X2luZ19uYW1lX2NvbCgpYAogICMgZnVuY3Rpb24uIFRoZW4gd2Ugc2F2ZSBhIHZlY3RvciBvZiBhbGwgdGhlIGluZ3JlZGllbnQgY29sdW1uIG5hbWVzIGluIGBpbmdyZWRpZW50X2NvbG5hbWVzYC4gSXQgd2lsbCBzdGF5CiAgIyBjb25zdGFudCBldmVuIGlmIHRoZSBpbmRpY2VzIGNoYW5nZSB3aGVuIHdlIHNlbGVjdCBvdXQgY2VydGFpbiBjb2x1bW5zLiAKICAKICAjIEZpcnN0IGluZ3JlZGllbnQKICBmaXJzdF9pbmdyZWRpZW50X25hbWUgPC0gcGFzdGUoaW5ncmVkaWVudF93YW50LCAiX25hbWVfMSIsIHNlcD0iIikKICBmaXJzdF9pbmdyZWRpZW50X2luZGV4IDwtIHdoaWNoKGNvbG5hbWVzKGRmKT09Zmlyc3RfaW5ncmVkaWVudF9uYW1lKQogIAogICMgR2V0IHRoZSBsYXN0IGluZ3JlZGllbnQKICBnZXRfbGFzdF9pbmdfbmFtZV9jb2wgPC0gZnVuY3Rpb24oZGYpIHsKICAgIGZvciAoY29sIGluIG5hbWVzKGRmKSkgewogICAgICBpZiAoZ3JlcGwocGFzdGUoaW5ncmVkaWVudF93YW50LCAiX25hbWVfIiwgc2VwID0gIiIpLCBjb2wpID09IFRSVUUpIHsKICAgICAgICBuYW1lX2xhc3RfaW5nX2NvbCA8LSBjb2wKICAgICAgfQogICAgfQogICAgcmV0dXJuKG5hbWVfbGFzdF9pbmdfY29sKQogIH0KICAKICAjIExhc3QgaW5ncmVkaWVudAogIGxhc3RfaW5ncmVkaWVudF9uYW1lIDwtIGdldF9sYXN0X2luZ19uYW1lX2NvbChkZikKICBsYXN0X2luZ3JlZGllbnRfaW5kZXggPC0gd2hpY2goY29sbmFtZXMoZGYpPT1sYXN0X2luZ3JlZGllbnRfbmFtZSkKICAKICAjIFZlY3RvciBvZiBhbGwgdGhlIGluZ3JlZGllbnQgY29sdW1uIG5hbWVzCiAgaW5ncmVkaWVudF9jb2xuYW1lcyA8LSBuYW1lcyhkZilbZmlyc3RfaW5ncmVkaWVudF9pbmRleDpsYXN0X2luZ3JlZGllbnRfaW5kZXhdCiAgCiAgIyBOb24taW5ncmVkaWVudCBjb2x1bW4gbmFtZXMgd2Ugd2FudCB0byBrZWVwCiAgdG9fa2VlcF9jb2xfbmFtZXMgPC0gYygiY2x1c3Rlcl9hc3NpZ25tZW50IiwgIm5hbWUiLCAiYWJ2IiwgImlidSIsICJzcm0iLCAic3R5bGUiLCAic3R5bGVfY29sbGFwc2VkIikKICAKICAjIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0jIAogIAogICMgSW5zaWRlIGBnYXRoZXJfaW5ncmVkaWVudHMoKWAgd2UgdGFrZSBvdXQgc3VwZXJmbG91cyBjb2x1bW4gbmFtZXMgdGhhdCBhcmUgbm90IGluIGB0b19rZWVwX2NvbF9uYW1lc2Agb3Igb25lIAogICMgb2YgdGhlIGluZ3JlZGllbnQgY29sdW1ucywgZmluZCB3aGF0IHRoZSBuZXcgaW5ncmVkaWVudCBjb2x1bW4gaW5kaWNlcyBhcmUsIHNpbmNlIHRoZXknbGwgaGF2ZSBjaGFuZ2VkIGFmdGVyIAogICMgd2UgcGFyZWQgZG93biBhbmQgdGhlbiBnYXRoZXIgYWxsIG9mIHRoZSBpbmdyZWRpZW50IGNvbHVtbnMgKGUuZy4sIGBob3BzX25hbWVfMWApIGludG8gb25lIGxvbmcgY29sdW1uLCAKICAjIGBpbmdfa2V5c2AgYW5kIGFsbCB0aGUgYWN0dWFsIGluZ3JlZGllbnQgbmFtZXMgKGUuZy4sIENhc2NhZGUpIGludG8gYGluZ19uYW1lc2AuCiAgCiAgIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBHYXRoZXIgY29sdW1ucyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIwogIGdhdGhlcl9pbmdyZWRpZW50cyA8LSBmdW5jdGlvbihkZiwgY29sc190b19nYXRoZXIpIHsKICAgIHRvX2tlZXBfaW5kaWNlcyA8LSB3aGljaChjb2xuYW1lcyhkZikgJWluJSB0b19rZWVwX2NvbF9uYW1lcykKICAgIAogICAgc2VsZWN0ZWRfZGYgPC0gZGZbLCBjKHRvX2tlZXBfaW5kaWNlcywgZmlyc3RfaW5ncmVkaWVudF9pbmRleDpsYXN0X2luZ3JlZGllbnRfaW5kZXgpXQogICAgCiAgICBuZXdfaW5nX2luZGljZXMgPC0gd2hpY2goY29sbmFtZXMoc2VsZWN0ZWRfZGYpICVpbiUgY29sc190b19nYXRoZXIpICAgICMgaW5kaWNlcyB3aWxsIGhhdmUgY2hhbmdlZCBzaW5jZSB3ZSBwYXJlZCBkb3duIAogICAgCiAgICBkZl9nYXRoZXJlZCA8LSBzZWxlY3RlZF9kZiAlPiUKICAgICAgZ2F0aGVyXygKICAgICAgICBrZXlfY29sID0gImluZ19rZXlzIiwKICAgICAgICB2YWx1ZV9jb2wgPSAiaW5nX25hbWVzIiwKICAgICAgICBnYXRoZXJfY29scyA9IGNvbG5hbWVzKHNlbGVjdGVkX2RmKVtuZXdfaW5nX2luZGljZXNdCiAgICAgICkgJT4lCiAgICAgIG11dGF0ZSgKICAgICAgICBjb3VudCA9IDEKICAgICAgKQogICAgZGZfZ2F0aGVyZWQKICB9CiAgYmVlcl9nYXRoZXJlZCA8LSBnYXRoZXJfaW5ncmVkaWVudHMoZGYsIGluZ3JlZGllbnRfY29sbmFtZXMpICAjIGluZ3JlZGllbnQgY29sbmFtZXMgZGVmaW5lZCBhYm92ZSBmdW5jdGlvbgogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjIAogIAogICMgTmV4dCB3ZSBnZXQgYSB2ZWN0b3Igb2YgYWxsIGluZ3JlZGllbnQgbGV2ZWxzIGFuZCB0YWtlIG91dCB0aGUgb25lIHRoYXQncyBhbiBlbXB0eSBzdHJpbmcgYW5kIAogICMgdXNlIHRoaXMgdmVjdG9yIG9mIGluZ3JlZGllbnQgbGV2ZWxzIGluIGBzZWxlY3Rfc3ByZWFkX2NvbHMoKWAgYmVsb3cKCiAgIyBHZXQgYSB2ZWN0b3Igb2YgYWxsIGluZ3JlZGllbnQgbGV2ZWxzCiAgYmVlcl9nYXRoZXJlZCRpbmdfbmFtZXMgPC0gZmFjdG9yKGJlZXJfZ2F0aGVyZWQkaW5nX25hbWVzKQogIGluZ3JlZGllbnRfbGV2ZWxzIDwtIGxldmVscyhiZWVyX2dhdGhlcmVkJGluZ19uYW1lcykgCiAgCiAgIyBUYWtlIG91dCB0aGUgbGV2ZWwgdGhhdCdzIGp1c3QgYW4gZW1wdHkgc3RyaW5nCiAgdG9fa2VlcF9sZXZlbHMgPC0gIShjKDE6bGVuZ3RoKGluZ3JlZGllbnRfbGV2ZWxzKSkgJWluJSB3aGljaChpbmdyZWRpZW50X2xldmVscyA9PSAiIikpCiAgaW5ncmVkaWVudF9sZXZlbHMgPC0gaW5ncmVkaWVudF9sZXZlbHNbdG9fa2VlcF9sZXZlbHNdCiAgCiAgYmVlcl9nYXRoZXJlZCRpbmdfbmFtZXMgPC0gYXMuY2hhcmFjdGVyKGJlZXJfZ2F0aGVyZWQkaW5nX25hbWVzKQogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gIyAKICAKICAjIFRoZW4gd2Ugc3ByZWFkIHRoZSBpbmdyZWRpZW50IG5hbWVzOiB3ZSB0YWtlIHdoYXQgd2FzIHByZXZpb3VzbHkgdGhlIGB2YWx1ZWAgaW4gb3VyIGdhdGhlcmVkIGRhdGFmcmFtZSwgdGhlCiAgIyBhY3R1YWwgaW5ncmVkaWVudCBuYW1lcyAoQ2FzY2FkZSwgQ2VudGVubmlhbCkgYW5kIG1ha2UgdGhhdCBvdXIgYGtleWA7IGl0J2xsIGZvcm0gdGhlIG5ldyBjb2x1bW4gbmFtZXMuIFRoZQogICMgbmV3IGB2YWx1ZWAgaXMgYHZhbHVlYCBpcyBjb3VudDsgaXQnbGwgcG9wdWxhdGUgdGhlIHJvdyBjZWxscy4gSWYgYSBnaXZlbiByb3cgaGFzIGEgY2VydGFpbiBpbmdyZWRpZW50LCBpdAogICMgZ2V0cyBhIDEgaW4gdGhlIGNvcnJlc3BvbmRpbmcgY2VsbCwgYW4gTkEgb3RoZXJ3aXNlLiAKICAjIFdlIGFkZCBhIHVuaXF1ZSBpZGVuZml0aWVyIGZvciBlYWNoIHJvdyB3aXRoIGByb3dgLCB3aGljaCB3ZSdsbCBkcm9wIGxhdGVyIChzZWUgW0hhZGxleSdzIFNPCiAgIyBjb21tZW50XShodHRwczovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8yNTk2MDM5NC91bmV4cGVjdGVkLWJlaGF2aW9yLXdpdGgtdGlkeXIpKS4KCiAgCiAgIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tIFNwcmVhZCBjb2x1bW5zIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICMKICBzcHJlYWRfaW5ncmVkaWVudHMgPC0gZnVuY3Rpb24oZGYpIHsKICAgIGRmX3NwcmVhZCA8LSBkZiAlPiUgCiAgICAgIG11dGF0ZSgKICAgICAgICByb3cgPSAxOm5yb3coZGYpICAgICAgICAjIEFkZCBhIHVuaXF1ZSBpZGVuZml0aWVyIGZvciBlYWNoIHJvdyB3aGljaCB3ZSdsbCBuZWVkIGluIG9yZGVyIHRvIHNwcmVhZDsgd2UnbGwgZHJvcCB0aGlzIGxhdGVyCiAgICAgICkgJT4lICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgIHNwcmVhZCgKICAgICAgICBrZXkgPSBpbmdfbmFtZXMsCiAgICAgICAgdmFsdWUgPSBjb3VudAogICAgICApIAogICAgcmV0dXJuKGRmX3NwcmVhZCkKICB9CiAgYmVlcl9zcHJlYWQgPC0gc3ByZWFkX2luZ3JlZGllbnRzKGJlZXJfZ2F0aGVyZWQpCiAgIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICMgCgogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBTZWxlY3Qgb25seSBjZXJ0YWluIGNvbHVtbnMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjCiAgc2VsZWN0X3NwcmVhZF9jb2xzIDwtIGZ1bmN0aW9uKGRmKSB7CiAgICB0b19rZWVwX2NvbF9pbmRpY2VzIDwtIHdoaWNoKGNvbG5hbWVzKGRmKSAlaW4lIHRvX2tlZXBfY29sX25hbWVzKQogICAgdG9fa2VlcF9pbmdyZWRpZW50X2luZGljZXMgPC0gd2hpY2goY29sbmFtZXMoZGYpICVpbiUgaW5ncmVkaWVudF9sZXZlbHMpCiAgICAKICAgIHRvX2tlZXBfaW5kc19hbGwgPC0gYyh0b19rZWVwX2NvbF9pbmRpY2VzLCB0b19rZWVwX2luZ3JlZGllbnRfaW5kaWNlcykKICAgIAogICAgbmV3X2RmIDwtIGRmICU+JSAKICAgICAgc2VsZWN0XygKICAgICAgICAuZG90cyA9IHRvX2tlZXBfaW5kc19hbGwKICAgICAgKQogICAgcmV0dXJuKG5ld19kZikKICB9CiAgYmVlcl9zcHJlYWRfc2VsZWN0ZWQgPC0gc2VsZWN0X3NwcmVhZF9jb2xzKGJlZXJfc3ByZWFkKQogICMgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAjIAoKICAjIFRha2Ugb3V0IGFsbCByb3dzIHRoYXQgaGF2ZSBubyBpbmdyZWRpZW50cyBzcGVjaWZpZWQgYXQgYWxsCiAgaW5kc190b19yZW1vdmUgPC0gYXBwbHkoYmVlcl9zcHJlYWRfc2VsZWN0ZWRbLCBmaXJzdF9pbmdyZWRpZW50X2luZGV4Omxhc3RfaW5ncmVkaWVudF9pbmRleF0sIAogICAgICAgICAgICAgICAgICAgICAgICAgIDEsIGZ1bmN0aW9uKHgpIGFsbChpcy5uYSh4KSkpCiAgYmVlcl9zcHJlYWRfbm9fbmEgPC0gYmVlcl9zcHJlYWRfc2VsZWN0ZWRbICFpbmRzX3RvX3JlbW92ZSwgXQogIAogIAogICMgLS0tLS0tLS0tLS0tLS0tLS0gR3JvdXAgaW5ncmVkaWVudHMgYnkgdGhlIGdyb3VwZXIgc3BlY2lmaWVkIC0tLS0tLS0tLS0tLS0tLS0tLS0gIwogICMgVGhlbiB3ZSBkbyB0aGUgZmluYWwgc3RlcCBhbmQgZ3JvdXAgYnkgdGhlIGdyb3VwZXJzLgogIAogIGdldF9pbmdyZWRpZW50c19wZXJfZ3JvdXBlciA8LSBmdW5jdGlvbihkZiwgZ3JvdXBlciA9IGdyb3VwZXIpIHsKICAgIGRmX2dyb3VwZWQgPC0gZGYgJT4lCiAgICAgIHVuZ3JvdXAoKSAlPiUgCiAgICAgIGdyb3VwX2J5Xyhncm91cGVyKQogICAgCiAgICBub3RfZm9yX3N1bW1pbmcgPC0gd2hpY2goY29sbmFtZXMoZGZfZ3JvdXBlZCkgJWluJSB0b19rZWVwX2NvbF9uYW1lcykKICAgIG1heF9ub3RfZm9yX3N1bW1pbmcgPC0gbWF4KG5vdF9mb3Jfc3VtbWluZykKICAgIAogICAgcGVyX2dyb3VwZXIgPC0gZGZfZ3JvdXBlZCAlPiUgCiAgICAgIHNlbGVjdCgtYyhhYnYsIGlidSwgc3JtKSkgJT4lICAgICMgdGFraW5nIG91dCB0ZW1wb3JhcmlseQogICAgICBzdW1tYXJpc2VfaWYoCiAgICAgICAgaXMubnVtZXJpYywgICAgICAgICAgICAgIAogICAgICAgIHN1bSwgbmEucm0gPSBUUlVFCiAgICAgICAgIyAtYyhhYnYsIGlidSwgc3JtKQogICAgICApICU+JQogICAgICBtdXRhdGUoCiAgICAgICAgdG90YWwgPSByb3dTdW1zKC5bKG1heF9ub3RfZm9yX3N1bW1pbmcgKyAxKTpuY29sKC4pXSwgbmEucm0gPSBUUlVFKSAgICAKICAgICAgKQogICAgCiAgICAjIFNlbmQgdG90YWwgdG8gdGhlIHNlY29uZCBwb3NpdGlvbgogICAgcGVyX2dyb3VwZXIgPC0gcGVyX2dyb3VwZXIgJT4lIAogICAgICBzZWxlY3QoCiAgICAgICAgbmFtZSwgdG90YWwsIGV2ZXJ5dGhpbmcoKQogICAgICApCiAgICAKICAgICMgUmVwbGFjZSB0b3RhbCBjb2x1bW4gd2l0aCBtb3JlIGRlc2NyaXB0aXZlIG5hbWU6IHRvdGFsXzxpbmdyZWRpZW50PgogICAgbmFtZXMocGVyX2dyb3VwZXIpW3doaWNoKG5hbWVzKHBlcl9ncm91cGVyKSA9PSAidG90YWwiKV0gPC0gcGFzdGUwKCJ0b3RhbF8iLCBpbmdyZWRpZW50X3dhbnQpCiAgICAKICAgIHJldHVybihwZXJfZ3JvdXBlcikKICB9CiAgIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tICMgCiAgCiAgaW5ncmVkaWVudHNfcGVyX2dyb3VwZXIgPC0gZ2V0X2luZ3JlZGllbnRzX3Blcl9ncm91cGVyKGJlZXJfc3ByZWFkX3NlbGVjdGVkLCBncm91cGVyKQogIHJldHVybihpbmdyZWRpZW50c19wZXJfZ3JvdXBlcikKfQpgYGAKCgoqIE5vdyBydW4gdGhlIGZ1bmN0aW9uIHdpdGggYGluZ3JlZGllbnRfd2FudGAgYXMgZmlyc3QgaG9wcywgdGhlbiBtYWx0CiogVGhlbiBqb2luIHRoZSByZXN1bHRpbmcgZGF0YWZyYW1lcyBhbmQgcmVtb3ZlL3Jlb3JkZXIgc29tZSBjb2x1bW5zCgpgYGB7ciwgZWNobz1UUlVFLCBldmFsPVRSVUV9CiMgUnVuIHRoZSBlbnRpcmUgZnVuY3Rpb24gd2l0aCBpbmdyZWRpZW50X3dhbnQgc2V0IHRvIGhvcHMsIGdyb3VwaW5nIGJ5IG5hbWUKaW5ncmVkaWVudHNfcGVyX2JlZXJfaG9wcyA8LSBwaWNrX2luZ3JlZGllbnRfZ2V0X2JlZXIoaW5ncmVkaWVudF93YW50ID0gImhvcHMiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgYmVlcl9uZWNlc3NpdGllcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGdyb3VwZXIgPSBjKCJuYW1lIiwgInN0eWxlX2NvbGxhcHNlZCIpKQoKIyBTYW1lIGZvciBtYWx0CmluZ3JlZGllbnRzX3Blcl9iZWVyX21hbHQgPC0gcGlja19pbmdyZWRpZW50X2dldF9iZWVyKGluZ3JlZGllbnRfd2FudCA9ICJtYWx0IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGJlZXJfbmVjZXNzaXRpZXMsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBncm91cGVyID0gYygibmFtZSIsICJzdHlsZV9jb2xsYXBzZWQiKSkKCiMgSm9pbiB0aG9zZSBvbiBvdXIgb3JpZ2luYWwgZGF0YWZyYW1lIGJ5IG5hbWUKYmVlcl9pbmdyZWRpZW50c19qb2luX2ZpcnN0X2luZ3JlZGllbnQgPC0gbGVmdF9qb2luKGJlZXJfbmVjZXNzaXRpZXMsIGluZ3JlZGllbnRzX3Blcl9iZWVyX2hvcHMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBieSA9ICJuYW1lIikKYmVlcl9pbmdyZWRpZW50c19qb2luIDwtIGxlZnRfam9pbihiZWVyX2luZ3JlZGllbnRzX2pvaW5fZmlyc3RfaW5ncmVkaWVudCwgaW5ncmVkaWVudHNfcGVyX2JlZXJfbWFsdCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBieSA9ICJuYW1lIikKCgojIFRha2Ugb3V0IHNvbWUgdW5uZWNlc3NhcnkgY29sdW1ucwp1bm5lY2Vzc2FyeV9jb2xzIDwtIGMoInN0eWxlSWQiLCAiYWJ2X3NjYWxlZCIsICJpYnVfc2NhbGVkIiwgInNybV9zY2FsZWQiLCAKICAgICAgICAgICAgICAgICAgICAgICJob3BzX2lkIiwgIm1hbHRfaWQiLCAiZ2xhc3N3YXJlSWQiLCAic3R5bGUuY2F0ZWdvcnlJZCIpCmJlZXJfaW5ncmVkaWVudHNfam9pbiA8LSBiZWVyX2luZ3JlZGllbnRzX2pvaW5bLCAoISBuYW1lcyhiZWVyX2luZ3JlZGllbnRzX2pvaW4pICVpbiUgdW5uZWNlc3NhcnlfY29scyldCgoKIyBJZiB3ZSBhbHNvIHdhbnQgdG8gdGFrZSBvdXQgYW55IG9mIHRoZSBtYWx0X25hbWVfMSwgbWFsdF9uYW1lXzIsIGV0Yy4gY29sdW1ucyB3ZSBjYW4gZG8gdGhpcyB3aXRoIGEgZ3JlcAptb3JlX3VubmVjZXNzYXJ5IDwtIGMoImhvcHNfbmFtZV98bWFsdF9uYW1lXyIpCmJlZXJfaW5ncmVkaWVudHNfam9pbiA8LSAKICBiZWVyX2luZ3JlZGllbnRzX2pvaW5bLCAoISBncmVwbChtb3JlX3VubmVjZXNzYXJ5LCBuYW1lcyhiZWVyX2luZ3JlZGllbnRzX2pvaW4pKSA9PSBUUlVFKV0KCiMgUmVvcmRlciBjb2x1bW5zIGEgYml0CmJlZXJfaW5ncmVkaWVudHNfam9pbiA8LSBiZWVyX2luZ3JlZGllbnRzX2pvaW4gJT4lIAogIHNlbGVjdCgKICAgIGlkLCBuYW1lLCB0b3RhbF9ob3BzLCB0b3RhbF9tYWx0LCBldmVyeXRoaW5nKCksIC1kZXNjcmlwdGlvbgogICkKCiMgQW5kIGdldCBhIGRmIHRoYXQgaW5jbHVkZXMgdG90YWxfaG9wcyBhbmQgdG90YWxfbWFsdCBidXQgbm90IGFsbCB0aGUgb3RoZXIgaW5ncmVkaWVudCBjb2x1bW5zCmJlZXJfdG90YWxzIDwtIGJlZXJfaW5ncmVkaWVudHNfam9pbiAlPiUgCiAgc2VsZWN0KAogICAgaWQsIG5hbWUsIHRvdGFsX2hvcHMsIHRvdGFsX21hbHQsIHN0eWxlLCBzdHlsZV9jb2xsYXBzZWQsCiAgICBhYnYsIGlidSwgc3JtLCBnbGFzcywgaG9wc19uYW1lLCBtYWx0X25hbWUKICApCgpgYGAKCgoKTm93IHdlJ3JlIGxlZnQgd2l0aCBzb21ldGhpbmcgb2YgYSBzcGFyc2UgbWF0cml4IG9mIGFsbCB0aGUgaW5ncmVkaWVudHMgY29tcGFyZWQgdG8gYWxsIHRoZSBiZWVycwpgYGB7cn0Ka2FibGUoYmVlcl9pbmdyZWRpZW50c19qb2luWzE6MjAsIF0pCmBgYAoKCgoKKioqCgojIyMgVW5zdXBlcnZpc2VkIENsdXN0ZXJpbmcgCldlIEstbWVhbnMgY2x1c3RlciBiZWVycyBiYXNlZCBvbiBjZXJ0YWluIG51bWVyaWMgcHJlZGljdG9yIHZhcmlhYmxlcy4gCgoKKipQcmVwKioKCiogV3JpdGUgYSBmdW5jaXRvbiB0aGF0IHRha2VzIGEgZGF0YWZyYW1lLCBhIHNldCBvZiBwcmVkaWN0b3JzLCBhIHJlc3BvbnNlIHZhcmlhYmxlLCBhbmQgdGhlIG51bWJlciBvZiBjbHVzdGVyIGNlbnRlcnMgeW91IHdhbnQKICAgICogTkI6IFRoZXJlIGFyZSBub3Qgbm90IHZlcnkgbWFueSBiZWVycyBoYXZlIFNSTSBzbyB3ZSBtYXkgbm90IHdhbnQgdG8gb21pdCBiYXNlZCBvbiBpdAoKKiBUYWtlIG91dCBtaXNzaW5nIHZhbHVlcywgYW5kIHNjYWxlIHRoZSBkYXRhCiogVGFrZSBvdXQgb3V0bGllcnMsIGRlZmluZWQgYXMgYmVlcnMgaGF2ZSB0byBoYXZlIGFuIEFCViBiZXR3ZWVuIDMgYW5kIDIwIGFuZCBhbiBJQlUgbGVzcyB0aGFuIDIwMAoqIFRoZW4gY2x1c3RlciBvbiBqdXN0IHRoZSBwcmVkaWN0b3JzIGFuZCBjb21wYXJlIHRvIHRoZSByZXNwb25zZSB2YXJpYWJsZQogIAoKYGBge3IsIGVjaG89VFJVRX0KCmxpYnJhcnkoTmJDbHVzdCkKCmNsdXN0ZXJfaXQgPC0gZnVuY3Rpb24oZGYsIHByZWRzLCB0b19zY2FsZSwgcmVzcCwgbl9jZW50ZXJzKSB7CiAgZGZfZm9yX2NsdXN0ZXJpbmcgPC0gZGYgJT4lCiAgICBzZWxlY3RfKC5kb3RzID0gYyhyZXNwb25zZV92YXJzLCBjbHVzdGVyX29uKSkgJT4lCiAgICBuYS5vbWl0KCkgJT4lCiAgICBmaWx0ZXIoCiAgICAgIGFidiA8IDIwICYgYWJ2ID4gMwogICAgKSAlPiUKICAgIGZpbHRlcigKICAgICAgaWJ1IDwgMjAwCiAgICApCgogIGRmX2FsbF9wcmVkcyA8LSBkZl9mb3JfY2x1c3RlcmluZyAlPiUKICAgIHNlbGVjdF8oLmRvdHMgPSBwcmVkcykKCiAgZGZfcHJlZHNfc2NhbGUgPC0gZGZfYWxsX3ByZWRzICU+JQogICAgc2VsZWN0XyguZG90cyA9IHRvX3NjYWxlKSAlPiUKICAgIHJlbmFtZSgKICAgICAgYWJ2X3NjYWxlZCA9IGFidiwKICAgICAgaWJ1X3NjYWxlZCA9IGlidSwKICAgICAgc3JtX3NjYWxlZCA9IHNybQogICAgKSAlPiUKICAgIHNjYWxlKCkgJT4lCiAgICBhc190aWJibGUoKQoKICBkZl9wcmVkcyA8LSBiaW5kX2NvbHMoZGZfcHJlZHNfc2NhbGUsIGRmX2FsbF9wcmVkc1ssICghbmFtZXMoZGZfYWxsX3ByZWRzKSAlaW4lIHRvX3NjYWxlKV0pCgogIGRmX291dGNvbWUgPC0gZGZfZm9yX2NsdXN0ZXJpbmcgJT4lCiAgICBzZWxlY3RfKC5kb3RzID0gcmVzcCkgJT4lCiAgICBuYS5vbWl0KCkKCiAgc2V0LnNlZWQoOSkKICBjbHVzdGVyZWRfZGZfb3V0IDwtIGttZWFucyh4ID0gZGZfcHJlZHMsIGNlbnRlcnMgPSBuX2NlbnRlcnMsIHRyYWNlID0gVFJVRSkKCiAgY2x1c3RlcmVkX2RmIDwtIGFzX3RpYmJsZShkYXRhLmZyYW1lKAogICAgY2x1c3Rlcl9hc3NpZ25tZW50ID0gZmFjdG9yKGNsdXN0ZXJlZF9kZl9vdXQkY2x1c3RlciksCiAgICBkZl9vdXRjb21lLCBkZl9wcmVkcywKICAgIGRmX2Zvcl9jbHVzdGVyaW5nICU+JSBzZWxlY3QoYWJ2LCBpYnUsIHNybSkpKQoKICByZXR1cm4oY2x1c3RlcmVkX2RmKQp9CgpgYGAKCgoKIAoqKkNsdXN0ZXIqKgoKRmlyc3Qgd2UnbGwgcnVuIHRoZSBmdWN0aW9uIHdpdGggMTAgY2VudGVycywgYW5kIGNsdXN0ZXIgb24gdGhlIHByZWRpY3RvcnMgQUJWLCBJQlUsIFNSTSwgdG90YWxfaG9wcywgYW5kIHRvdGFsX21hbHQuCgoKYGBge3IsIGVjaG89VFJVRX0KCmNsdXN0ZXJfb24gPC0gYygiYWJ2IiwgImlidSIsICJzcm0iLCAidG90YWxfaG9wcyIsICJ0b3RhbF9tYWx0IikKdG9fc2NhbGUgPC0gYygiYWJ2IiwgImlidSIsICJzcm0iLCAidG90YWxfaG9wcyIsICJ0b3RhbF9tYWx0IikKcmVzcG9uc2VfdmFycyA8LSBjKCJuYW1lIiwgInN0eWxlIiwgInN0eWxlX2NvbGxhcHNlZCIpCgpjbHVzdGVyZWRfYmVlciA8LSBjbHVzdGVyX2l0KGRmID0gYmVlcl90b3RhbHMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJlZHMgPSBjbHVzdGVyX29uLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRvX3NjYWxlID0gdG9fc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzcCA9IHJlc3BvbnNlX3ZhcnMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9jZW50ZXJzID0gMTApCmBgYAoKCkhlYWQgb2YgdGhlIGNsdXN0ZXJpbmcgZGF0YQpgYGB7cn0Ka2FibGUoY2x1c3RlcmVkX2JlZXJbMToyMCwgXSkKYGBgCgoKCkEgdGFibGUgb2YgY2x1c3RlciBjb3VudHMgYnJva2VuIGRvd24gYnkgc3R5bGUKYGBge3J9CmNsdXN0ZXJfdGFibGVfY291bnRzIDwtIHRhYmxlKHN0eWxlID0gY2x1c3RlcmVkX2JlZXIkc3R5bGVfY29sbGFwc2VkLCBjbHVzdGVyID0gY2x1c3RlcmVkX2JlZXIkY2x1c3Rlcl9hc3NpZ25tZW50KQoKa2FibGUoY2x1c3Rlcl90YWJsZV9jb3VudHMpCmBgYAoKClBsb3QgdGhlIGNsdXN0ZXJzLiBUaGVyZSBhcmUgMyBheGVzOiBBQlYsIElCVSwgYW5kIFNSTSwgc28gd2UgY2hvb3NlIHR3byBhdCBhIHRpbWUuIAoKYGBge3IsIGVjaG89VFJVRX0KY2x1c3RlcmVkX2JlZXJfcGxvdF9hYnZfaWJ1IDwtIGdncGxvdChkYXRhID0gY2x1c3RlcmVkX2JlZXIsIGFlcyh4ID0gYWJ2LCB5ID0gaWJ1LCBjb2xvdXIgPSBjbHVzdGVyX2Fzc2lnbm1lbnQpKSArIAogIGdlb21faml0dGVyKCkgKyB0aGVtZV9taW5pbWFsKCkgICsKICBnZ3RpdGxlKCJrLU1lYW5zIENsdXN0ZXJpbmcgb2YgQmVlciBieSBBQlYsIElCVSwgU1JNIikgKwogIGxhYnMoeCA9ICJBQlYiLCB5ID0gIklCVSIpICsKICBsYWJzKGNvbG91ciA9ICJDbHVzdGVyIEFzc2lnbm1lbnQiKQpjbHVzdGVyZWRfYmVlcl9wbG90X2Fidl9pYnUKCmNsdXN0ZXJlZF9iZWVyX3Bsb3RfYWJ2X3NybSA8LSBnZ3Bsb3QoZGF0YSA9IGNsdXN0ZXJlZF9iZWVyLCBhZXMoeCA9IGFidiwgeSA9IHNybSwgY29sb3VyID0gY2x1c3Rlcl9hc3NpZ25tZW50KSkgKyAKICBnZW9tX2ppdHRlcigpICsgdGhlbWVfbWluaW1hbCgpICArCiAgZ2d0aXRsZSgiay1NZWFucyBDbHVzdGVyaW5nIG9mIEJlZXIgYnkgQUJWLCBJQlUsIFNSTSIpICsKICBsYWJzKHggPSAiQUJWIiwgeSA9ICJTUk0iKSArCiAgbGFicyhjb2xvdXIgPSAiQ2x1c3RlciBBc3NpZ25tZW50IikKY2x1c3RlcmVkX2JlZXJfcGxvdF9hYnZfc3JtCmBgYAoKCgpOb3cgd2UgY2FuIGFkZCBpbiB0aGUgc3R5bGUgY2VudGVycyAobWVhbnMpIGZvciBlYWNoIGBzdHlsZV9jb2xsYXBzZWRgIGFuZCBsYWJlbCBpdC4KCmBgYHtyLCBlY2hvPVRSVUV9CmxpYnJhcnkoZ2dyZXBlbCkKYWJ2X2lidV9jbHVzdGVyc192c19zdHlsZV9jZW50ZXJzIDwtIGdncGxvdCgpICsgICAKICBnZW9tX3BvaW50KGRhdGEgPSBjbHVzdGVyZWRfYmVlciwgCiAgICAgICAgICAgICBhZXMoeCA9IGFidiwgeSA9IGlidSwgY29sb3VyID0gY2x1c3Rlcl9hc3NpZ25tZW50KSwgYWxwaGEgPSAwLjUpICsKICBnZW9tX3BvaW50KGRhdGEgPSBzdHlsZV9jZW50ZXJzLAogICAgICAgICAgICAgYWVzKG1lYW5fYWJ2LCBtZWFuX2lidSksIGNvbG91ciA9ICJibGFjayIpICsKICBnZW9tX3RleHRfcmVwZWwoZGF0YSA9IHN0eWxlX2NlbnRlcnMsIGFlcyhtZWFuX2FidiwgbWVhbl9pYnUsIGxhYmVsID0gc3R5bGVfY29sbGFwc2VkKSwgCiAgICAgICAgICAgICAgICAgIGJveC5wYWRkaW5nID0gdW5pdCgwLjQ1LCAibGluZXMiKSwKICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gIkNhbGlicmkiLAogICAgICAgICAgICAgICAgICBsYWJlbC5zaXplID0gMC4zKSArCiAgZ2d0aXRsZSgiUG9wdWxhciBTdHlsZXMgdnMuIGstTWVhbnMgQ2x1c3RlcmluZyBvZiBCZWVyIGJ5IEFCViwgSUJVLCBTUk0iKSArCiAgbGFicyh4ID0gIkFCViIsIHkgPSAiSUJVIikgKwogIGxhYnMoY29sb3VyID0gIkNsdXN0ZXIgQXNzaWdubWVudCIpICsKICB0aGVtZV9idygpCmFidl9pYnVfY2x1c3RlcnNfdnNfc3R5bGVfY2VudGVycwpgYGAKCgpUaGUgY2x1c3RlcmluZyBhYm92ZSB1c2VkIGEgc21hbGxlciBudW1iZXIgb2YgY2x1c3RlcnMgKDEwKSB0aGFuIHRoZXJlIGFyZSBgc3R5bGVzX2NvbGxhcHNlZGAuIFRoYXQgbWFrZXMgaXQgZGlmZmljdWx0IHRvIGRldGVybWluZSB3aGV0aGVyIGEgZ2l2ZW4gc3R5bGUgZml0cyBzbnVnbHkgaW50byBhIGNsdXN0ZXIgb3Igbm90LgoKCgoqKkNsdXN0ZXIgb24ganVzdCBjZXJ0YWluIHNlbGVjdGVkIHN0eWxlcyoqCgpXZSdsbCB0YWtlIGZpdmUgdmVyeSBkaXN0aW5jdCBjb2xsYXBzZWQgc3R5bGVzIGFuZCByZS1ydW4gdGhlIGNsdXN0ZXJpbmcgb24gYmVlcnMgdGhhdCBmYWxsIGludG8gdGhlc2UgY2F0ZWdvcmllcy4gClRoZXNlIHN0eWxlcyB3ZXJlIGludGVudGlvbmFsbHkgY2hvc2VuIGJlY2F1c2UgdGhleSBhcmUgcXVpdGUgZGlzdGluY3Q6IEJsb25kZSwgSVBBLCBTdG91dCwgVHJpcGVsLCBXaGVhdC4gQXJndWFibHksIG9mIHRoZXNlIGZpdmUgc3R5bGVzIEJsb25kZXMgYW5kIFdoZWF0cyBhcmUgdGhlIGNsb3Nlc3QKCgoKYGBge3IsIGVjaG89VFJVRX0Kc3R5bGVzX3RvX2tlZXAgPC0gYygiQmxvbmRlIiwgIkluZGlhIFBhbGUgQWxlIiwgIlN0b3V0IiwgIlRyaXBlbCIsICJXaGVhdCIpCmJ0X2NlcnRhaW5fc3R5bGVzIDwtIGJlZXJfdG90YWxzICU+JQogIGZpbHRlcigKICAgIHN0eWxlX2NvbGxhcHNlZCAlaW4lIHN0eWxlc190b19rZWVwCiAgKQoKCmNsdXN0ZXJfb24gPC0gYygiYWJ2IiwgImlidSIsICJzcm0iLCAidG90YWxfaG9wcyIsICJ0b3RhbF9tYWx0IikKdG9fc2NhbGUgPC0gYygiYWJ2IiwgImlidSIsICJzcm0iLCAidG90YWxfaG9wcyIsICJ0b3RhbF9tYWx0IikKcmVzcG9uc2VfdmFycyA8LSBjKCJuYW1lIiwgInN0eWxlIiwgInN0eWxlX2NvbGxhcHNlZCIpCgpjZXJ0YWluX3N0eWxlc19jbHVzdGVyZWQgPC0gY2x1c3Rlcl9pdChkZiA9IGJ0X2NlcnRhaW5fc3R5bGVzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkcyA9IGNsdXN0ZXJfb24sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRvX3NjYWxlID0gdG9fc2NhbGUsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc3AgPSByZXNwb25zZV92YXJzLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2NlbnRlcnMgPSA1KQoKc3R5bGVfY2VudGVyc19jZXJ0YWluX3N0eWxlcyA8LSBzdHlsZV9jZW50ZXJzICU+JSAKICBmaWx0ZXIoc3R5bGVfY29sbGFwc2VkICVpbiUgc3R5bGVzX3RvX2tlZXApCmBgYAoKCgoKClRhYmxlIG9mIHN0eWxlIHZzLiBjbHVzdGVyLgpgYGB7cn0Ka2FibGUodGFibGUoc3R5bGUgPSBjZXJ0YWluX3N0eWxlc19jbHVzdGVyZWQkc3R5bGVfY29sbGFwc2VkLCBjbHVzdGVyID0gY2VydGFpbl9zdHlsZXNfY2x1c3RlcmVkJGNsdXN0ZXJfYXNzaWdubWVudCkpCmBgYAoKCgpOb3cgdGhhdCB3ZSBoYXZlIGEgbWFuYWdlYWJsZSBudW1iZXIgb2Ygc3R5bGVzLCB3ZSBjYW4gc2VlIGhvdyB3ZWxsIGZpdCBlYWNoIGNsdXN0ZXIgaXMgdG8gZWFjaCBzdHlsZS4gSWYgdGhlIGZlYXR1cmVzIHdlIGNsdXN0ZXJlZCBvbiBwZXJmZWN0bHkgcHJlZGljdGVkIHN0eWxlLCB0aGVyZSB3b3VsZCBlYWNoIGNvbG9yIChjbHVzdGVyKSB3b3VsZCBiZSB1bmlxdWUgdG8gZWFjaCBmYWNldCBvZiB0aGUgcGxvdC4gKEUuZy4sIGxlZnQgZW50aXJlbHkgYmx1ZSwgc2Vjb25kIGZyb20gbGVmdCBlbnRpcmVseSBncmVlbiwgZXRjLikKCgoKYGBge3IsIGVjaG89VFJVRX0KYnlfc3R5bGVfcGxvdCA8LSBnZ3Bsb3QoKSArICAgCiAgZ2VvbV9wb2ludChkYXRhID0gY2VydGFpbl9zdHlsZXNfY2x1c3RlcmVkLCAKICAgICAgICAgICAgIGFlcyh4ID0gYWJ2LCB5ID0gaWJ1LAogICAgICAgICAgICAgICAgIGNvbG91ciA9IGNsdXN0ZXJfYXNzaWdubWVudCksIGFscGhhID0gMC41KSArCiAgZmFjZXRfZ3JpZCguIH4gc3R5bGVfY29sbGFwc2VkKSArCiAgZ2VvbV9wb2ludChkYXRhID0gc3R5bGVfY2VudGVyc19jZXJ0YWluX3N0eWxlcywKICAgICAgICAgICBhZXMobWVhbl9hYnYsIG1lYW5faWJ1KSwgY29sb3VyID0gImJsYWNrIiwgc2hhcGUgPSA1KSArCiAgZ2d0aXRsZSgiU2VsZWN0ZWQgU3R5bGVzIENsdXN0ZXIgQXNzaWdubWVudCIpICsKICBsYWJzKHggPSAiQUJWIiwgeSA9ICJJQlUiKSArCiAgbGFicyhjb2xvdXIgPSAiQ2x1c3RlciIpICsKICB0aGVtZV9idygpCmJ5X3N0eWxlX3Bsb3QKYGBgCgoKCgo8IS0tICMjIyBCYWNrIHRvIGNsdXN0ZXJpbmc6IGNsdXN0ZXIgb24gb25seSA1IHN0eWxlcyAtLT4KCjwhLS0gKiBXZSdsbCBwYXJlIGRvd24gdGhlIGJlZXIgZGF0YSB0byBqdXN0IGJlZXJzIGluIDUgc2VsZWN0ZWQgc3R5bGVzOyAKPCEtLSAqIFdlJ2xsIGNsdXN0ZXIgdGhlc2UgaW50byA1IGNsdXN0ZXJzIC0tPgo8IS0tIDwhLS0gKiBUaGlzIHRpbWUgd2UnbGwgYWRkIGluIGB0b3RhbF9ob3BzYCBhbmQgYHRvdGFsX21hbHRgIGFzIHByZWRpY3RvcnMgIC0tPiAtLT4KCgoKCjwhLS0gZ2dwbG90KCkgKyAtLT4KPCEtLSAgIGdlb21fcG9pbnQoZGF0YSA9IGNlcnRhaW5fc3R5bGVzX2NsdXN0ZXJlZCwgLS0+CjwhLS0gICAgICAgICAgICAgIGFlcyh4ID0gYWJ2LCB5ID0gaWJ1LCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgIHNoYXBlID0gY2x1c3Rlcl9hc3NpZ25tZW50LCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgIGNvbG91ciA9IHN0eWxlX2NvbGxhcHNlZCksIGFscGhhID0gMC41KSArIC0tPgo8IS0tICAgZ2VvbV9wb2ludChkYXRhID0gc3R5bGVfY2VudGVyc19jZXJ0YWluX3N0eWxlcywgLS0+CjwhLS0gICAgICAgICAgICAgIGFlcyhtZWFuX2FidiwgbWVhbl9pYnUpLCBjb2xvdXIgPSAiYmxhY2siKSArIC0tPgo8IS0tICAgZ2VvbV90ZXh0X3JlcGVsKGRhdGEgPSBzdHlsZV9jZW50ZXJzX2NlcnRhaW5fc3R5bGVzLCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgICBhZXMobWVhbl9hYnYsIG1lYW5faWJ1LCBsYWJlbCA9IHN0eWxlX2NvbGxhcHNlZCksIC0tPgo8IS0tICAgICAgICAgICAgICAgICAgIGJveC5wYWRkaW5nID0gdW5pdCgwLjQ1LCAibGluZXMiKSwgLS0+CjwhLS0gICAgICAgICAgICAgICAgICAgZmFtaWx5ID0gIkNhbGlicmkiLCAtLT4KPCEtLSAgICAgICAgICAgICAgICAgICBsYWJlbC5zaXplID0gMC4zKSArIC0tPgo8IS0tICAgZ2d0aXRsZSgiU2VsZWN0ZWQgU3R5bGVzIChjb2xvcnMpIG1hdGNoZWQgd2l0aCBDbHVzdGVyIEFzc2lnbm1lbnRzIChzaGFwZXMpIikgKyAtLT4KPCEtLSAgIGxhYnMoeCA9ICJBQlYiLCB5ID0gIklCVSIpICsgLS0+CjwhLS0gICBsYWJzKGNvbG91ciA9ICJTdHlsZSIsIHNoYXBlID0gIkNsdXN0ZXIgQXNzaWdubWVudCIpICsgLS0+CjwhLS0gICB0aGVtZV9idygpIC0tPgoKPCEtLSBgYGAgLS0+CgoKCiMjIFJhbmRvbSBhc2lkZXMgaW50byBob3BzCgoqKkRvIG1vcmUgaG9wcyBhbHdheXMgbWVhbiBtb3JlIGJpdHRlcm5lc3M/KioKCiogSXQgd291bGQgYXBwZWFyIHNvLCBmcm9tIHRoaXMgZ3JhcGggYW5kIHRoaXMgcmVncmVzc2lvbiAoYmV0YSA9IDIuMzk0NDE4KQpgYGB7ciwgZWNobz1UUlVFfQoKZ2dwbG90KGRhdGEgPSBiZWVyX2luZ3JlZGllbnRzX2pvaW4sIGFlcyh0b3RhbF9ob3BzLCBpYnUpKSArCiAgZ2VvbV9wb2ludChhZXModG90YWxfaG9wcywgaWJ1LCBjb2xvdXIgPSBzdHlsZV9jb2xsYXBzZWQpKSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kID0gbG0sIHNlID0gRkFMU0UsIGNvbG91ciA9ICJibGFjayIpICsgCiAgZ2d0aXRsZSgiSG9wcyBQZXIgQmVlciB2cy4gQml0dGVybmVzcyIpICsKICBsYWJzKHggPSAiTnVtYmVyIG9mIEhvcHMiLCB5ID0gIklCVSIsIGNvbG91ciA9ICJTdHlsZSBDb2xsYXBzZWQiKSArCiAgdGhlbWVfbWluaW1hbCgpCgpob3BzX2lidV9sbSA8LSBsbShpYnUgfiB0b3RhbF9ob3BzLCBkYXRhID0gYmVlcl9pbmdyZWRpZW50c19qb2luKQpzdW1tYXJ5KGhvcHNfaWJ1X2xtKQpgYGAKCiogSG93ZXZlciwgcGFzdCBhIGNlcnRhaW4gcG9pbnQgKDMgaG9wcyBvciBtb3JlKSwgdGhlcmUncyBubyBlZmZlY3Qgb2YgbnVtYmVyIG9mIGhvcHMgb24gSUJVCmBgYHtyLCBlY2hvPVRSVUV9CmdncGxvdChkYXRhID0gYmVlcl9pbmdyZWRpZW50c19qb2luW3doaWNoKGJlZXJfaW5ncmVkaWVudHNfam9pbiR0b3RhbF9ob3BzID4gMgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAmIGJlZXJfaW5ncmVkaWVudHNfam9pbiR0b3RhbF9ob3BzIDwgOCksIF0sIGFlcyh0b3RhbF9ob3BzLCBpYnUpKSArCiAgZ2VvbV9wb2ludChhZXModG90YWxfaG9wcywgaWJ1LCBjb2xvdXIgPSBzdHlsZV9jb2xsYXBzZWQpKSArCiAgZ2VvbV9zbW9vdGgobWV0aG9kID0gbG0sIHNlID0gRkFMU0UsIGNvbG91ciA9ICJibGFjayIpICsKICBnZ3RpdGxlKCIzKyBIb3BzIFBlciBCZWVyIHZzLiBCaXR0ZXJuZXNzIikgKwogIGxhYnMoeCA9ICJOdW1iZXIgb2YgSG9wcyIsIHkgPSAiSUJVIiwgY29sb3VyID0gIlN0eWxlIENvbGxhcHNlZCIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgoKKipNb3N0IHBvcHVsYXIgaG9wcyoqCgpgYGB7ciwgZWNobz1UUlVFfQojIEdhdGhlciB1cCBhbGwgdGhlIGhvcHMgY29sdW1ucyBpbnRvIG9uZSBjYWxsZWQgYGhvcF9uYW1lYApiZWVyX25lY2Vzc2l0aWVzX2hvcHNfZ2F0aGVyZWQgPC0gYmVlcl9uZWNlc3NpdGllcyAlPiUKICBnYXRoZXIoCiAgICBob3Bfa2V5LCBob3BfbmFtZSwgaG9wc19uYW1lXzE6aG9wc19uYW1lXzEzCiAgKSAlPiUgYXNfdGliYmxlKCkKCiMgRmlsdGVyIHRvIGp1c3QgdGhvc2UgYmVlcnMgdGhhdCBoYXZlIGF0IGxlYXN0IG9uZSBob3AKYmVlcl9uZWNlc3NpdGllc193X2hvcHMgPC0gYmVlcl9uZWNlc3NpdGllc19ob3BzX2dhdGhlcmVkICU+JSAKICBmaWx0ZXIoIWlzLm5hKGhvcF9uYW1lKSkgJT4lIAogIGZpbHRlcighaG9wX25hbWUgPT0gIiIpCgpiZWVyX25lY2Vzc2l0aWVzX3dfaG9wcyRob3BfbmFtZSA8LSBmYWN0b3IoYmVlcl9uZWNlc3NpdGllc193X2hvcHMkaG9wX25hbWUpCgojIEZvciBhbGwgaG9wcywgZmluZCB0aGUgbnVtYmVyIG9mIGJlZXJzIHRoZXkncmUgaW4gYXMgd2VsbCBhcyB0aG9zZSBiZWVycycgbWVhbiBJQlUgYW5kIEFCVgpob3BzX2JlZXJfc3RhdHMgPC0gYmVlcl9uZWNlc3NpdGllc193X2hvcHMgJT4lIAogIHVuZ3JvdXAoKSAlPiUgCiAgZ3JvdXBfYnkoaG9wX25hbWUpICU+JSAKICBzdW1tYXJpc2UoCiAgICBtZWFuX2lidSA9IG1lYW4oaWJ1LCBuYS5ybSA9IFRSVUUpLCAKICAgIG1lYW5fYWJ2ID0gbWVhbihhYnYsIG5hLnJtID0gVFJVRSksCiAgICBuID0gbigpCiAgKQoKIyBQYXJlIHRvIGhvcHMgdGhhdCBhcmUgdXNlZCBpbiBhdCBsZWFzdCA1MCBiZWVycwpwb3BfaG9wc19iZWVyX3N0YXRzIDwtIGhvcHNfYmVlcl9zdGF0c1tob3BzX2JlZXJfc3RhdHMkbiA+IDUwLCBdCmthYmxlKHBvcF9ob3BzX2JlZXJfc3RhdHMpCgojIEtlZXAganVzdCBiZWVycyB0aGF0IGNvbnRhaW4gdGhlc2UgbW9zdCBwb3B1bGFyIGhvcHMKYmVlcl9uZWNlc3NpdGllc193X3BvcHVsYXJfaG9wcyA8LSBiZWVyX25lY2Vzc2l0aWVzX3dfaG9wcyAlPiUgCiAgZmlsdGVyKGhvcF9uYW1lICVpbiUgcG9wX2hvcHNfYmVlcl9zdGF0cyRob3BfbmFtZSkgJT4lIAogIGRyb3BsZXZlbHMoKSAKYGBgCgpBcmUgdGhlcmUgY2VydGlhbiBob3BzIHRoYXQgYXJlIHVzZWQgbW9yZSBvZnRlbiBpbiB2ZXJ5IGhpZ2ggSUJVIG9yIEFCViBiZWVycz8KSGFyZCB0byBkZXRlY3QgYSBwYXR0ZXJuCmBgYHtyLCBlY2hvID0gVFJVRX0KZ2dwbG90KGRhdGEgPSBiZWVyX25lY2Vzc2l0aWVzX3dfcG9wdWxhcl9ob3BzKSArIAogIGdlb21fcG9pbnQoYWVzKGFidiwgaWJ1LCBjb2xvdXIgPSBob3BfbmFtZSkpICsKICBnZ3RpdGxlKCJCZWVycyBDb250YWluaW5nIG1vc3QgUG9wdWxhciBIb3BzIikgKwogIGxhYnMoeCA9ICJBQlYiLCB5ID0gIklCVSIsIGNvbG91ciA9ICJIb3AgTmFtZSIpICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgpgYGB7ciwgZWNobz1UUlVFfQpnZ3Bsb3QoZGF0YSA9IHBvcF9ob3BzX2JlZXJfc3RhdHMpICsgCiAgZ2VvbV9wb2ludChhZXMobWVhbl9hYnYsIG1lYW5faWJ1LCBjb2xvdXIgPSBob3BfbmFtZSwgc2l6ZSA9IG4pKSArCiAgZ2d0aXRsZSgiTW9zdCBQb3B1bGFyIEhvcHMnIEVmZmVjdCBvbiBBbGNvaG9sIGFuZCBCaXR0ZXJuZXNzIikgKwogIGxhYnMoeCA9ICJNZWFuIEFCViBwZXIgSG9wIFR5cGUiLCB5ID0gIk1lYW4gSUJVIHBlciBIb3AgVHlwZSIsIGNvbG91ciA9ICJIb3AgTmFtZSIsIAogICAgICAgc2l6ZSA9ICJOdW1iZXIgb2YgQmVlcnMiKSArCiAgdGhlbWVfbWluaW1hbCgpCmBgYAoKCiMgTmV1cmFsIE5ldAoKKiBDYW4gQUJWLCBJQlUsIGFuZCBTUk0gYmUgdXNlZCBpbiBhIG5ldXJhbCBuZXQgdG8gcHJlZGljdCBgc3R5bGVgIG9yIGBzdHlsZV9jb2xsYXBzZWRgPwoqIEluIHRoZSBmdW5jdGlvbiwgc3BlY2lmeSB0aGUgZGF0YWZyYW1lIGFuZCB0aGUgb3V0Y29tZSwgZWl0aGVyIGBzdHlsZWAgb3IgYHN0eWxlX2NvbGxhcHNlZGA7IHRoZSBvbmUgbm90IHNwZWNpZmllZCBhcyBgb3V0Y29tZWAgd2lsbCBiZSBkcm9wcGVkCiogVGhlIHByZWRpY3RvciBjb2x1bW5zIHdpbGwgYmUgZXZlcnl0aGluZyBub3Qgc3BlY2lmaWVkIGluIHRoZSB2ZWN0b3IgYHByZWRpY3Rvcl92YXJzYAoqIFRoZSBmdW5jdGlvbiByZXR1cm5zIHRoZSBvdXRjb21lIHZhcmlhYmxlIHNsZWVjdGVkLCBuZXVyYWwgbmV0IG91dHB1dCwgdmFyaWFibGUgaW1wb3J0YW5jZSwgdGhlIHByZWRpY3Rpb24gZGF0YWZyYW1lLCBwcmVkaWN0aW9ucywgYW5kIGFjY3VyYWN5CgpgYGB7ciwgd2FybmluZz1GQUxTRSwgZWNobz1UUlVFLCBldmFsPVRSVUUsIG1lc3NhZ2U9RkFMU0V9CgpsaWJyYXJ5KG5uZXQpCmxpYnJhcnkoY2FyZXQpCgpydW5fbmV1cmFsX25ldCA8LSBmdW5jdGlvbihkZiwgb3V0Y29tZSwgcHJlZGljdG9yX3ZhcnMpIHsKICBvdXQgPC0gbGlzdChvdXRjb21lID0gb3V0Y29tZSkKICAKICAjIENyZWF0ZSBhIG5ldyBjb2x1bW4gb3V0Y29tZTsgaXQncyBzdHlsZV9jb2xsYXBzZWQgaWYgeW91IHNldCBvdXRjb21lIHRvIHN0eWxlX2NvbGxhcHNlZCwgYW5kIHN0eWxlIG90aGVyd2lzZQogIGlmIChvdXRjb21lID09ICJzdHlsZV9jb2xsYXBzZWQiKSB7CiAgICBkZltbIm91dGNvbWUiXV0gPC0gZGZbWyJzdHlsZV9jb2xsYXBzZWQiXV0KICB9IGVsc2UgewogICAgZGZbWyJvdXRjb21lIl1dIDwtIGRmW1sic3R5bGUiXV0KICB9CgogIGRmJG91dGNvbWUgPC0gZmFjdG9yKGRmJG91dGNvbWUpCiAgCiAgY29sc190b19rZWVwIDwtIGMoIm91dGNvbWUiLCBwcmVkaWN0b3JfdmFycykKICAKICBkZiA8LSBkZiAlPiUKICAgIHNlbGVjdF8oLmRvdHMgPSBjb2xzX3RvX2tlZXApICU+JQogICAgbXV0YXRlKHJvdyA9IDE6bnJvdyhkZikpICU+JSAKICAgIGRyb3BsZXZlbHMoKQoKICAjIFNlbGVjdCA4MCUgb2YgdGhlIGRhdGEgZm9yIHRyYWluaW5nCiAgZGZfdHJhaW4gPC0gc2FtcGxlX24oZGYsIG5yb3coZGYpKigwLjgpKQogIAogICMgVGhlIHJlc3QgaXMgZm9yIHRlc3RpbmcKICBkZl90ZXN0IDwtIGRmICU+JQogICAgZmlsdGVyKCEgKHJvdyAlaW4lIGRmX3RyYWluJHJvdykpICU+JQogICAgc2VsZWN0KC1yb3cpCiAgCiAgZGZfdHJhaW4gPC0gZGZfdHJhaW4gJT4lCiAgICBzZWxlY3QoLXJvdykKICAKICAjIEJ1aWxkIG11bHRpbm9tYWlsIG5ldXJhbCBuZXQKICBubiA8LSBtdWx0aW5vbShvdXRjb21lIH4gLiwKICAgICAgICAgICAgICAgICBkYXRhID0gZGZfdHJhaW4sIG1heGl0PTUwMCwgdHJhY2U9VCkKCiAgIyBXaGljaCB2YXJpYWJsZXMgYXJlIHRoZSBtb3N0IGltcG9ydGFudCBpbiB0aGUgbmV1cmFsIG5ldD8KICBtb3N0X2ltcG9ydGFudF92YXJzIDwtIHZhckltcChubikKCiAgIyBIb3cgYWNjdXJhdGUgaXMgdGhlIG1vZGVsPyBDb21wYXJlIHByZWRpY3Rpb25zIHRvIG91dGNvbWVzIGZyb20gdGVzdCBkYXRhCiAgbm5fcHJlZHMgPC0gcHJlZGljdChubiwgdHlwZT0iY2xhc3MiLCBuZXdkYXRhID0gZGZfdGVzdCkKICBubl9hY2N1cmFjeSA8LSBwb3N0UmVzYW1wbGUoZGZfdGVzdCRvdXRjb21lLCBubl9wcmVkcykKCiAgb3V0IDwtIGxpc3Qob3V0LCBubiA9IG5uLCBtb3N0X2ltcG9ydGFudF92YXJzID0gbW9zdF9pbXBvcnRhbnRfdmFycywKICAgICAgICAgICAgICBkZl90ZXN0ID0gZGZfdGVzdCwKICAgICAgICAgICAgICBubl9wcmVkcyA9IG5uX3ByZWRzLAogICAgICAgICAgIG5uX2FjY3VyYWN5ID0gbm5fYWNjdXJhY3kpCgogIHJldHVybihvdXQpCn0KCmBgYAoKKiBTZXQgdGhlIGRhdGFmcmFtZSB0byBiZSBgYmVlcl90b3RhbHNgLCB0aGUgcHJlZGljdG9yIHZhcmlhYmxlcyB0byBiZSB0aGUgdmVjdG9yIGNvbnRhaW5lZCBpbiBgcF92YXJzYCwgdGhlIG91dGNvbWUgdG8gYmUgYHN0eWxlX2NvbGxhcHNlZGAKCmBgYHtyLCBlY2hvPVRSVUUsIGV2YWw9VFJVRX0KcF92YXJzIDwtIGMoInRvdGFsX2hvcHMiLCAidG90YWxfbWFsdCIsICJhYnYiLCAiaWJ1IiwgInNybSIpCgpubl9jb2xsYXBzZWRfb3V0IDwtIHJ1bl9uZXVyYWxfbmV0KGRmID0gYmVlcl90b3RhbHMsIG91dGNvbWUgPSAic3R5bGVfY29sbGFwc2VkIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICBwcmVkaWN0b3JfdmFycyA9IHBfdmFycykKCgojIEhvdyBhY2N1cmF0ZSB3YXMgaXQ/Cm5uX2NvbGxhcHNlZF9vdXQkbm5fYWNjdXJhY3kKCiMgV2hhdCB3ZXJlIHRoZSBtb3N0IGltcG9ydGFudCB2YXJpYWJsZXM/Cm5uX2NvbGxhcHNlZF9vdXQkbW9zdF9pbXBvcnRhbnRfdmFycwoKYGBgCgoKKiBXaGF0IGlmIHdlIHByZWRjaXQgYHN0eWxlYCBpbnN0ZWFkIG9mIGBzdHlsZV9jb2xsYXBzZWRgPwoKYGBge3IsIGVjaG89VFJVRX0KCm5uX25vdGNvbGxhcHNlZF9vdXQgPC0gcnVuX25ldXJhbF9uZXQoZGYgPSBiZWVyX3RvdGFscywgb3V0Y29tZSA9ICJzdHlsZSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgcHJlZGljdG9yX3ZhcnMgPSBwX3ZhcnMpCgpubl9ub3Rjb2xsYXBzZWRfb3V0JG5uX2FjY3VyYWN5Cgpubl9ub3Rjb2xsYXBzZWRfb3V0JG1vc3RfaW1wb3J0YW50X3ZhcnMKCmBgYAoKCkFuZCBub3cgaWYgd2UgYWRkIGBnbGFzc2AgYXMgYSBwcmVkaWN0b3I/CmBgYHtyLCBlY2hvPVRSVUV9CgpwX3ZhcnNfYWRkX2dsYXNzIDwtIGMoInRvdGFsX2hvcHMiLCAidG90YWxfbWFsdCIsICJhYnYiLCAiaWJ1IiwgInNybSIpCgpubl9jb2xsYXBzZWRfb3V0X2FkZF9nbGFzcyA8LSBydW5fbmV1cmFsX25ldChkZiA9IGJlZXJfaW5ncmVkaWVudHNfam9pbiwgb3V0Y29tZSA9ICJzdHlsZV9jb2xsYXBzZWQiLCAKICAgICAgICAgICAgICAgICAgICAgICAgIHByZWRpY3Rvcl92YXJzID0gcF92YXJzX25vX2dsYXNzKQoKbm5fY29sbGFwc2VkX291dF9hZGRfZ2xhc3Mkbm5fYWNjdXJhY3kKCm5uX2NvbGxhcHNlZF9vdXRfYWRkX2dsYXNzJG1vc3RfaW1wb3J0YW50X3ZhcnMKCmBgYAoKCgoKIyMjIFJhbmRvbSBmb3Jlc3Qgd2l0aCBhbGwgaW5ncmVkaWVudHMKCiogV2UgY2FuIHVzZSBhIHJhbmRvbSBmb3Jlc3QgdG8gZ2V0IGV2ZW4gbW9yZSBncmFudWxhciB3aXRoIGluZ3JlZGllbnRzCiAgICAqIFRoZSBzcGFyc2UgaW5ncmVkaWVudCBkYXRhZnJhbWUgd2FzIHRvbyBjb21wbGV4IGZvciB0aGUgbXVsdGlub21pYWwgbmV1cmFsIG5ldDsgaG93ZXZlciwgd2UgY2FuIAoKKiBIZXJlIHdlIGRvbid0IGluY2x1ZGUgYGdsYXNzYCBhcyBhIHByZWRpY3RvcgoKYGBge3IsIGVjaG89VFJVRX0KCmxpYnJhcnkocmFuZ2VyKQpsaWJyYXJ5KHN0cmluZ3IpCgpiaSA8LSBiZWVyX2luZ3JlZGllbnRzX2pvaW4gJT4lIAogIHNlbGVjdCgtYyhpZCwgbmFtZSwgY2x1c3Rlcl9hc3NpZ25tZW50LCBzdHlsZSwgaG9wc19uYW1lLCBtYWx0X25hbWUsCiAgICAgICAgICAgIGdsYXNzKSkgJT4lIAogIG11dGF0ZShyb3cgPSAxOm5yb3coLikpIAoKYmkkc3R5bGVfY29sbGFwc2VkIDwtIGZhY3RvcihiaSRzdHlsZV9jb2xsYXBzZWQpCgoKIyByYW5nZXIgY29tcGxhaW5zIGFib3V0IHNwZWNpYWwgY2hhcmFjdGVycyBhbmQgc3BhY2VzIGluIGluZ3JlZGllbnQgY29sdW1uIG5hbWVzLiBUYWtlIHRoZW0gb3V0IGFuZCByZXBsYWNlIHdpdGggZW1wdHkgc3RyaW5nLgpuYW1lcyhiaSkgPC0gdG9sb3dlcihuYW1lcyhiaSkpCm5hbWVzKGJpKSA8LSBzdHJfcmVwbGFjZV9hbGwobmFtZXMoYmkpLCAiICIsICIiKQpuYW1lcyhiaSkgPC0gc3RyX3JlcGxhY2VfYWxsKG5hbWVzKGJpKSwgIihbXFwoXFwpLVxcLycpXSspIiwgIiIpCgojIEtlZXAgODAlIGZvciB0cmFpbmluZwpiaV90cmFpbiA8LSBzYW1wbGVfbihiaSwgbnJvdyhiaSkqKDAuOCkpCgojIFRoZSByZXN0IGlzIGZvciB0ZXN0aW5nCmJpX3Rlc3QgPC0gYmkgJT4lCiAgZmlsdGVyKCEgKHJvdyAlaW4lIGJpX3RyYWluJHJvdykpICU+JQogIGRwbHlyOjpzZWxlY3QoLXJvdykKCmJpX3RyYWluIDwtIGJpX3RyYWluICU+JQogIGRwbHlyOjpzZWxlY3QoLXJvdykKCgpiaV9yZiA8LSByYW5nZXIoc3R5bGVfY29sbGFwc2VkIH4gLiwgZGF0YSA9IGJpX3RyYWluLCBpbXBvcnRhbmNlID0gImltcHVyaXR5Iiwgc2VlZCA9IDExKQpgYGAKCgpPT0IgKG91dCBvZiBiYWcpIHByZWRpY3Rpb24gZXJyb3IgaXMgYXJvdW5kIDU4JQogICAgKiBUaGlzIGNhbGN1bGF0ZWQgZnJvbSB0cmVlIHNhbXBsZXMgY29uc3RydWN0ZWQgYnV0IG5vdCB1c2VkIGluIHRyYWluaW5nIHNldDsgdGhlc2UgdHJlZXMgYmVjb21lIGVmZmVjdGl2ZWx5IHBhcnQgb2YgdGVzdCBzZXQKYGBge3J9CmJpX3JmCmBgYAoKCldlIGNhbiBjb21wYXJlIHByZWRpY3RlZCBjbGFzc2lmaWNhdGlvbiBvbiB0aGUgdGVzdCBzZXQgdG8gdGhlaXIgYWN0dWFsIHN0eWxlIGNsYXNzaWZpY2F0aW9uLgpgYGB7ciwgZWNobz1UUlVFfQpwcmVkX2JpX3JmIDwtIHByZWRpY3QoYmlfcmYsIGRhdCA9IGJpX3Rlc3QpCnRhYmxlKGJpX3Rlc3Qkc3R5bGVfY29sbGFwc2VkLCBwcmVkX2JpX3JmJHByZWRpY3Rpb25zKQpgYGAKCgpWYXJpYWJsZSBpbXBvcnRhbmNlCiogSW50ZXJlc3RpbmdseSwgQUJWLCBJQlUsIGFuZCBTUk0gYXJlIGFsbCBtdWNoIG1vcmUgaW1wb3J0YW50IGluIHRoZSByYW5kb20gZm9yZXN0IHRoYW4gYHRvdGFsX2hvcHNgIGFuZCBgdG90YWxfbWFsdGAKYGBge3IsIGVjaG89VFJVRX0KCmltcG9ydGFuY2UoYmlfcmYpWzE6MTBdCmBgYAoKCkhvdyBkb2VzIGEgQ1NSRiAoY2FzZS1zcGVjaWZpYyByYW5kb20gZm9yZXN0KSBmYXJlPwoKYGBge3IsIGVjaG89VFJVRX0KCmJpX2NzcmYgPC0gY3NyZihzdHlsZV9jb2xsYXBzZWQgfiAuLCB0cmFpbmluZ19kYXRhID0gYmlfdHJhaW4sIHRlc3RfZGF0YSA9IGJpX3Rlc3QsCiAgICAgICAgICAgICAgICBwYXJhbXMxID0gbGlzdChudW0udHJlZXMgPSA1LCBtdHJ5ID0gNCksCiAgICAgICAgICAgICAgICBwYXJhbXMyID0gbGlzdChudW0udHJlZXMgPSAyKSkKCmNzcmZfYWNjIDwtIHBvc3RSZXNhbXBsZShiaV9jc3JmLCBiaV90ZXN0JHN0eWxlX2NvbGxhcHNlZCkKCmNzcmZfYWNjCmBgYAoKCiFbXSguL3BvdXIuanBnKQoKCgojIyMgRmluYWwgVGhvdWdodHMKCgoqU3R5bGUgZmlyc3QsIGZvcmdpdmVuZXNzIGxhdGVyPyoKCiogT25lIHJlYXNvbiAgc2VlbXMgdGhhdCBiZWVycyBhcmUgZ2VuZXJhbGx5IGJyZXdlZCB3aXRoIHN0eWxlIGluIG1pbmQgZmlyc3QgKCJsZXQncyBtYWtlIGEgcGFsZSBhbGUiKSByYXRoZXIgdGhhbiBkZWNpZGluZyB0aGUgYmVlcidzIHN0eWxlIGFmdGVyIGRldGVybWluaW5nIGl0cyBjaGFyYWN0ZXJpc3RpY3MgYW5kIGlkaW9zeW5jcmFzaWVzIAogICAgKiBFdmVuIGlmIHRoZSBiZWVyIHR1cm5zIG91dCBtb3JlIGxpa2UgYSBzb3VyLCBhbmQgaW4gYSBibGluZCB0YXN0ZSB0ZXN0IG1pZ2h0IGJlIGNsYXNzaWZpZWQgYXMgYSBzb3VyIG1vcmUgb2Z0ZW4gdGhhbiBhIHBhbGUgYWxlLCBpdCBzdGlsbCBnZXRzIHRoZSBsYWJlbCBwYWxlIGFsZQogICAgKiBUaGlzIG1ha2VzIHRoZSBzdHlsZSBkZWZpbml0aW9ucyBicm9hZGVyIGFuZCBoYXJkZXIgdG8gcHJlZGljdAoKCgoqRnV0dXJlIERpcmVjdGlvbnMqCgoqIEluY29ycG9yYXRlIGZsYXZvciBwcm9maWxlcyBmb3IgYmVlcnMgc291cmNlZC9zY3JhcGVkIGZyb20gc29tZXdoZXJlCiogSW1wbGVtZW50IGEgR0FOIHRvIGNvbWUgdXAgd2l0aCBiZWVyIG5hbWVzCiogTW9yZSBvbiB0aGUgaG9wcyBkZWVwIGRpdmU6IHdoaWNoIGhvcHMgYXJlIHVzZWQgbW9zdCBvZnRlbiBpbiB3aGljaCBzdHlsZXM/CgoKCgoK